diff --git a/graphql-contracts b/graphql-contracts index 48cd334..e2627cb 160000 --- a/graphql-contracts +++ b/graphql-contracts @@ -1 +1 @@ -Subproject commit 48cd334d24af33107630f0bacd84e4b5af81ecb1 +Subproject commit e2627cb00dd1671b6091306e008ec64c610ec472 diff --git a/src/demo-workspace.ts b/src/demo-workspace.ts index a889816..fc7d818 100644 --- a/src/demo-workspace.ts +++ b/src/demo-workspace.ts @@ -99,6 +99,7 @@ const demoRoutes: DemoRoute[] = [ const demoNote = JSON.stringify({ seed: 'empty-team-workspace' }) const demoPricingCountryCode = process.env.DEMO_PRICING_COUNTRY_CODE ?? 'RU' +const demoClientCurrency = process.env.DEMO_CLIENT_CURRENCY ?? 'RUB' function stableUuid(teamUuid: string, entity: string, index: number): string { const hex = createHash('sha256') @@ -128,6 +129,7 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom const needsPricingBackfill = demoQuotationPricingCount > 0 || demoOrderPricingCount > 0 const kyc = needsCalculations || needsOrders || needsPricingBackfill ? await ensureDemoKycPricingContext({ + teamUuid: access.teamUuid, userId: access.userId, countryCode: demoPricingCountryCode, }) @@ -179,6 +181,7 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom const price = await lockCurrencyPrice({ sourceAmount: route.amountUsd, sourceCurrency: 'USD', + clientCurrency: demoClientCurrency, pricingCountryCode: kyc.countryCode, }) const pricingEffectiveDate = new Date(`${price.rateEffectiveDate}T00:00:00.000Z`) diff --git a/src/pricing-context.ts b/src/pricing-context.ts index 40fddd6..fba9011 100644 --- a/src/pricing-context.ts +++ b/src/pricing-context.ts @@ -4,7 +4,7 @@ type GraphqlEnvelope = { } export type KycPricingContext = { - userId: string + teamUuid: string profileUuid: string countryCode: string resolvedAt: string @@ -56,39 +56,40 @@ async function requestGraphql( } export async function resolveKycPricingContext( - userId: string, + teamUuid: string, ): Promise { const data = await requestGraphql<{ - pricingContextForUser: KycPricingContext | null + pricingContextForTeam: KycPricingContext | null }>( kycM2mEndpoint, - `query OrdersKycPricingContext($userId: String!) { - pricingContextForUser(userId: $userId) { - userId + `query OrdersKycPricingContext($teamUuid: String!) { + pricingContextForTeam(teamUuid: $teamUuid) { + teamUuid profileUuid countryCode resolvedAt } }`, - { userId }, + { teamUuid }, ) - if (data.pricingContextForUser === null) { - throw new Error(`Active KYC pricing context is required for user ${userId}`) + if (data.pricingContextForTeam === null) { + throw new Error(`Active KYC pricing context is required for team ${teamUuid}`) } - return data.pricingContextForUser + return data.pricingContextForTeam } export async function ensureDemoKycPricingContext(input: { + teamUuid: string userId: string countryCode: string }): Promise { const data = await requestGraphql<{ - ensureDemoPricingContextForUser: KycPricingContext + ensureDemoPricingContextForTeam: KycPricingContext }>( kycM2mEndpoint, - `mutation OrdersEnsureDemoKycPricingContext($userId: String!, $countryCode: String!) { - ensureDemoPricingContextForUser(userId: $userId, countryCode: $countryCode) { - userId + `mutation OrdersEnsureDemoKycPricingContext($teamUuid: String!, $userId: String!, $countryCode: String!) { + ensureDemoPricingContextForTeam(teamUuid: $teamUuid, userId: $userId, countryCode: $countryCode) { + teamUuid profileUuid countryCode resolvedAt @@ -96,12 +97,13 @@ export async function ensureDemoKycPricingContext(input: { }`, input, ) - return data.ensureDemoPricingContextForUser + return data.ensureDemoPricingContextForTeam } export async function lockCurrencyPrice(input: { sourceAmount: number sourceCurrency: string + clientCurrency: string pricingCountryCode: string rateDate?: string }): Promise { diff --git a/src/schemas/team.ts b/src/schemas/team.ts index 45232af..b37e7bb 100644 --- a/src/schemas/team.ts +++ b/src/schemas/team.ts @@ -306,6 +306,7 @@ export const teamTypeDefs = `#graphql } input CreateQuotationInput { + clientCurrency: String title: String status: String operationCode: String @@ -333,6 +334,7 @@ export const teamTypeDefs = `#graphql } input UpdateQuotationInput { + clientCurrency: String title: String status: String operationCode: String @@ -779,6 +781,7 @@ function buildCreateQuotationData(input: CreateQuotationInput, ctx: { teamUuid: return { teamUuid: ctx.teamUuid, createdByUserId: ctx.userId, + clientCurrency: normalizeUpper(input.clientCurrency) ?? 'USD', title: normalizeString(input.title) ?? 'Quotation', status: normalizeString(input.status) ?? 'draft', operationCode: normalizeUpper(input.operationCode), @@ -809,6 +812,9 @@ function buildCreateQuotationData(input: CreateQuotationInput, ctx: { teamUuid: function buildUpdateQuotationData(input: UpdateQuotationInput): Prisma.QuotationUpdateInput { const data: Prisma.QuotationUpdateInput = {} + if (hasOwn(input, 'clientCurrency')) { + data.clientCurrency = normalizeRequiredUpdateUpper(input.clientCurrency) + } if (hasOwn(input, 'title')) data.title = normalizeRequiredUpdateString(input.title) if (hasOwn(input, 'status')) data.status = normalizeRequiredUpdateString(input.status) if (hasOwn(input, 'operationCode')) data.operationCode = normalizeUpper(input.operationCode) @@ -975,24 +981,19 @@ function actorLabel(ctx: { userId: string; teamUuid: string }): string { async function lockTariffPrice( tariff: PrismaTariffReference, sourceAmount: number, - userId: string, + teamUuid: string, + clientCurrency: string, ): Promise<{ kyc: KycPricingContext; price: LockedCurrencyPrice }> { - const kyc = await resolveKycPricingContext(userId) + const kyc = await resolveKycPricingContext(teamUuid) const price = await lockCurrencyPrice({ sourceAmount, sourceCurrency: tariff.sourceCurrency, + clientCurrency, pricingCountryCode: kyc.countryCode, }) return { kyc, price } } -function quotationPricingUserId(quotation: PrismaQuotation): string { - if (!quotation.createdByUserId) { - throw new GraphQLError('Quotation owner is required to resolve KYC pricing currency') - } - return quotation.createdByUserId -} - function lockedQuotationPriceData( locked: { kyc: KycPricingContext; price: LockedCurrencyPrice } | null, currentClientCurrency: string, @@ -1053,7 +1054,8 @@ async function refreshQuotationSelection(quotationUuid: string, teamUuid: string ? await lockTariffPrice( bestMatch.tariff, bestMatch.projectedSourceAmount, - quotationPricingUserId(quotation), + quotation.teamUuid, + quotation.clientCurrency, ) : null @@ -1231,12 +1233,13 @@ export const teamResolvers = { }) const matches = computeTariffMatches(quotation, tariffs) - const kyc = await resolveKycPricingContext(quotationPricingUserId(quotation)) + const kyc = await resolveKycPricingContext(quotation.teamUuid) return Promise.all( matches.map(async item => { const price = await lockCurrencyPrice({ sourceAmount: item.projectedSourceAmount, sourceCurrency: item.tariff.sourceCurrency, + clientCurrency: quotation.clientCurrency, pricingCountryCode: kyc.countryCode, }) return { @@ -1316,6 +1319,9 @@ export const teamResolvers = { updateQuotation: async (_: unknown, args: { quotationUuid: string; input: UpdateQuotationInput }, ctx: AuthContext) => { const access = assertTeamAccess(ctx) const current = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid) + if (hasOwn(args.input, 'clientCurrency') && current.status === 'converted') { + throw new GraphQLError('Converted quotation currency is fixed') + } const teamAccess = { teamUuid: current.teamUuid, userId: access.userId } const actor = { userId: access.userId, label: actorLabel(teamAccess) } await prisma.quotation.update({ @@ -1357,7 +1363,8 @@ export const teamResolvers = { const locked = await lockTariffPrice( tariff, projectedSourceAmount, - quotationPricingUserId(quotation), + quotation.teamUuid, + quotation.clientCurrency, ) await prisma.quotation.update({ where: {