Fix transaction currency on quotations
Build and deploy Docker image / build (push) Successful in 51s

This commit is contained in:
Ruslan Bakiev
2026-08-24 21:10:09 +07:00
parent 2f1aa30d6b
commit d34db71b51
4 changed files with 40 additions and 28 deletions
+3
View File
@@ -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`)
+17 -15
View File
@@ -4,7 +4,7 @@ type GraphqlEnvelope<T> = {
}
export type KycPricingContext = {
userId: string
teamUuid: string
profileUuid: string
countryCode: string
resolvedAt: string
@@ -56,39 +56,40 @@ async function requestGraphql<T>(
}
export async function resolveKycPricingContext(
userId: string,
teamUuid: string,
): Promise<KycPricingContext> {
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<KycPricingContext> {
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<LockedCurrencyPrice> {
+19 -12
View File
@@ -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: {