Fix transaction currency on quotations
Build and deploy Docker image / build (push) Successful in 51s
Build and deploy Docker image / build (push) Successful in 51s
This commit is contained in:
+1
-1
Submodule graphql-contracts updated: 48cd334d24...e2627cb00d
@@ -99,6 +99,7 @@ const demoRoutes: DemoRoute[] = [
|
|||||||
|
|
||||||
const demoNote = JSON.stringify({ seed: 'empty-team-workspace' })
|
const demoNote = JSON.stringify({ seed: 'empty-team-workspace' })
|
||||||
const demoPricingCountryCode = process.env.DEMO_PRICING_COUNTRY_CODE ?? 'RU'
|
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 {
|
function stableUuid(teamUuid: string, entity: string, index: number): string {
|
||||||
const hex = createHash('sha256')
|
const hex = createHash('sha256')
|
||||||
@@ -128,6 +129,7 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
|||||||
const needsPricingBackfill = demoQuotationPricingCount > 0 || demoOrderPricingCount > 0
|
const needsPricingBackfill = demoQuotationPricingCount > 0 || demoOrderPricingCount > 0
|
||||||
const kyc = needsCalculations || needsOrders || needsPricingBackfill
|
const kyc = needsCalculations || needsOrders || needsPricingBackfill
|
||||||
? await ensureDemoKycPricingContext({
|
? await ensureDemoKycPricingContext({
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
userId: access.userId,
|
userId: access.userId,
|
||||||
countryCode: demoPricingCountryCode,
|
countryCode: demoPricingCountryCode,
|
||||||
})
|
})
|
||||||
@@ -179,6 +181,7 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
|||||||
const price = await lockCurrencyPrice({
|
const price = await lockCurrencyPrice({
|
||||||
sourceAmount: route.amountUsd,
|
sourceAmount: route.amountUsd,
|
||||||
sourceCurrency: 'USD',
|
sourceCurrency: 'USD',
|
||||||
|
clientCurrency: demoClientCurrency,
|
||||||
pricingCountryCode: kyc.countryCode,
|
pricingCountryCode: kyc.countryCode,
|
||||||
})
|
})
|
||||||
const pricingEffectiveDate = new Date(`${price.rateEffectiveDate}T00:00:00.000Z`)
|
const pricingEffectiveDate = new Date(`${price.rateEffectiveDate}T00:00:00.000Z`)
|
||||||
|
|||||||
+17
-15
@@ -4,7 +4,7 @@ type GraphqlEnvelope<T> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type KycPricingContext = {
|
export type KycPricingContext = {
|
||||||
userId: string
|
teamUuid: string
|
||||||
profileUuid: string
|
profileUuid: string
|
||||||
countryCode: string
|
countryCode: string
|
||||||
resolvedAt: string
|
resolvedAt: string
|
||||||
@@ -56,39 +56,40 @@ async function requestGraphql<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveKycPricingContext(
|
export async function resolveKycPricingContext(
|
||||||
userId: string,
|
teamUuid: string,
|
||||||
): Promise<KycPricingContext> {
|
): Promise<KycPricingContext> {
|
||||||
const data = await requestGraphql<{
|
const data = await requestGraphql<{
|
||||||
pricingContextForUser: KycPricingContext | null
|
pricingContextForTeam: KycPricingContext | null
|
||||||
}>(
|
}>(
|
||||||
kycM2mEndpoint,
|
kycM2mEndpoint,
|
||||||
`query OrdersKycPricingContext($userId: String!) {
|
`query OrdersKycPricingContext($teamUuid: String!) {
|
||||||
pricingContextForUser(userId: $userId) {
|
pricingContextForTeam(teamUuid: $teamUuid) {
|
||||||
userId
|
teamUuid
|
||||||
profileUuid
|
profileUuid
|
||||||
countryCode
|
countryCode
|
||||||
resolvedAt
|
resolvedAt
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
{ userId },
|
{ teamUuid },
|
||||||
)
|
)
|
||||||
if (data.pricingContextForUser === null) {
|
if (data.pricingContextForTeam === null) {
|
||||||
throw new Error(`Active KYC pricing context is required for user ${userId}`)
|
throw new Error(`Active KYC pricing context is required for team ${teamUuid}`)
|
||||||
}
|
}
|
||||||
return data.pricingContextForUser
|
return data.pricingContextForTeam
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureDemoKycPricingContext(input: {
|
export async function ensureDemoKycPricingContext(input: {
|
||||||
|
teamUuid: string
|
||||||
userId: string
|
userId: string
|
||||||
countryCode: string
|
countryCode: string
|
||||||
}): Promise<KycPricingContext> {
|
}): Promise<KycPricingContext> {
|
||||||
const data = await requestGraphql<{
|
const data = await requestGraphql<{
|
||||||
ensureDemoPricingContextForUser: KycPricingContext
|
ensureDemoPricingContextForTeam: KycPricingContext
|
||||||
}>(
|
}>(
|
||||||
kycM2mEndpoint,
|
kycM2mEndpoint,
|
||||||
`mutation OrdersEnsureDemoKycPricingContext($userId: String!, $countryCode: String!) {
|
`mutation OrdersEnsureDemoKycPricingContext($teamUuid: String!, $userId: String!, $countryCode: String!) {
|
||||||
ensureDemoPricingContextForUser(userId: $userId, countryCode: $countryCode) {
|
ensureDemoPricingContextForTeam(teamUuid: $teamUuid, userId: $userId, countryCode: $countryCode) {
|
||||||
userId
|
teamUuid
|
||||||
profileUuid
|
profileUuid
|
||||||
countryCode
|
countryCode
|
||||||
resolvedAt
|
resolvedAt
|
||||||
@@ -96,12 +97,13 @@ export async function ensureDemoKycPricingContext(input: {
|
|||||||
}`,
|
}`,
|
||||||
input,
|
input,
|
||||||
)
|
)
|
||||||
return data.ensureDemoPricingContextForUser
|
return data.ensureDemoPricingContextForTeam
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lockCurrencyPrice(input: {
|
export async function lockCurrencyPrice(input: {
|
||||||
sourceAmount: number
|
sourceAmount: number
|
||||||
sourceCurrency: string
|
sourceCurrency: string
|
||||||
|
clientCurrency: string
|
||||||
pricingCountryCode: string
|
pricingCountryCode: string
|
||||||
rateDate?: string
|
rateDate?: string
|
||||||
}): Promise<LockedCurrencyPrice> {
|
}): Promise<LockedCurrencyPrice> {
|
||||||
|
|||||||
+19
-12
@@ -306,6 +306,7 @@ export const teamTypeDefs = `#graphql
|
|||||||
}
|
}
|
||||||
|
|
||||||
input CreateQuotationInput {
|
input CreateQuotationInput {
|
||||||
|
clientCurrency: String
|
||||||
title: String
|
title: String
|
||||||
status: String
|
status: String
|
||||||
operationCode: String
|
operationCode: String
|
||||||
@@ -333,6 +334,7 @@ export const teamTypeDefs = `#graphql
|
|||||||
}
|
}
|
||||||
|
|
||||||
input UpdateQuotationInput {
|
input UpdateQuotationInput {
|
||||||
|
clientCurrency: String
|
||||||
title: String
|
title: String
|
||||||
status: String
|
status: String
|
||||||
operationCode: String
|
operationCode: String
|
||||||
@@ -779,6 +781,7 @@ function buildCreateQuotationData(input: CreateQuotationInput, ctx: { teamUuid:
|
|||||||
return {
|
return {
|
||||||
teamUuid: ctx.teamUuid,
|
teamUuid: ctx.teamUuid,
|
||||||
createdByUserId: ctx.userId,
|
createdByUserId: ctx.userId,
|
||||||
|
clientCurrency: normalizeUpper(input.clientCurrency) ?? 'USD',
|
||||||
title: normalizeString(input.title) ?? 'Quotation',
|
title: normalizeString(input.title) ?? 'Quotation',
|
||||||
status: normalizeString(input.status) ?? 'draft',
|
status: normalizeString(input.status) ?? 'draft',
|
||||||
operationCode: normalizeUpper(input.operationCode),
|
operationCode: normalizeUpper(input.operationCode),
|
||||||
@@ -809,6 +812,9 @@ function buildCreateQuotationData(input: CreateQuotationInput, ctx: { teamUuid:
|
|||||||
function buildUpdateQuotationData(input: UpdateQuotationInput): Prisma.QuotationUpdateInput {
|
function buildUpdateQuotationData(input: UpdateQuotationInput): Prisma.QuotationUpdateInput {
|
||||||
const data: 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, 'title')) data.title = normalizeRequiredUpdateString(input.title)
|
||||||
if (hasOwn(input, 'status')) data.status = normalizeRequiredUpdateString(input.status)
|
if (hasOwn(input, 'status')) data.status = normalizeRequiredUpdateString(input.status)
|
||||||
if (hasOwn(input, 'operationCode')) data.operationCode = normalizeUpper(input.operationCode)
|
if (hasOwn(input, 'operationCode')) data.operationCode = normalizeUpper(input.operationCode)
|
||||||
@@ -975,24 +981,19 @@ function actorLabel(ctx: { userId: string; teamUuid: string }): string {
|
|||||||
async function lockTariffPrice(
|
async function lockTariffPrice(
|
||||||
tariff: PrismaTariffReference,
|
tariff: PrismaTariffReference,
|
||||||
sourceAmount: number,
|
sourceAmount: number,
|
||||||
userId: string,
|
teamUuid: string,
|
||||||
|
clientCurrency: string,
|
||||||
): Promise<{ kyc: KycPricingContext; price: LockedCurrencyPrice }> {
|
): Promise<{ kyc: KycPricingContext; price: LockedCurrencyPrice }> {
|
||||||
const kyc = await resolveKycPricingContext(userId)
|
const kyc = await resolveKycPricingContext(teamUuid)
|
||||||
const price = await lockCurrencyPrice({
|
const price = await lockCurrencyPrice({
|
||||||
sourceAmount,
|
sourceAmount,
|
||||||
sourceCurrency: tariff.sourceCurrency,
|
sourceCurrency: tariff.sourceCurrency,
|
||||||
|
clientCurrency,
|
||||||
pricingCountryCode: kyc.countryCode,
|
pricingCountryCode: kyc.countryCode,
|
||||||
})
|
})
|
||||||
return { kyc, price }
|
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(
|
function lockedQuotationPriceData(
|
||||||
locked: { kyc: KycPricingContext; price: LockedCurrencyPrice } | null,
|
locked: { kyc: KycPricingContext; price: LockedCurrencyPrice } | null,
|
||||||
currentClientCurrency: string,
|
currentClientCurrency: string,
|
||||||
@@ -1053,7 +1054,8 @@ async function refreshQuotationSelection(quotationUuid: string, teamUuid: string
|
|||||||
? await lockTariffPrice(
|
? await lockTariffPrice(
|
||||||
bestMatch.tariff,
|
bestMatch.tariff,
|
||||||
bestMatch.projectedSourceAmount,
|
bestMatch.projectedSourceAmount,
|
||||||
quotationPricingUserId(quotation),
|
quotation.teamUuid,
|
||||||
|
quotation.clientCurrency,
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
@@ -1231,12 +1233,13 @@ export const teamResolvers = {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const matches = computeTariffMatches(quotation, tariffs)
|
const matches = computeTariffMatches(quotation, tariffs)
|
||||||
const kyc = await resolveKycPricingContext(quotationPricingUserId(quotation))
|
const kyc = await resolveKycPricingContext(quotation.teamUuid)
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
matches.map(async item => {
|
matches.map(async item => {
|
||||||
const price = await lockCurrencyPrice({
|
const price = await lockCurrencyPrice({
|
||||||
sourceAmount: item.projectedSourceAmount,
|
sourceAmount: item.projectedSourceAmount,
|
||||||
sourceCurrency: item.tariff.sourceCurrency,
|
sourceCurrency: item.tariff.sourceCurrency,
|
||||||
|
clientCurrency: quotation.clientCurrency,
|
||||||
pricingCountryCode: kyc.countryCode,
|
pricingCountryCode: kyc.countryCode,
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
@@ -1316,6 +1319,9 @@ export const teamResolvers = {
|
|||||||
updateQuotation: async (_: unknown, args: { quotationUuid: string; input: UpdateQuotationInput }, ctx: AuthContext) => {
|
updateQuotation: async (_: unknown, args: { quotationUuid: string; input: UpdateQuotationInput }, ctx: AuthContext) => {
|
||||||
const access = assertTeamAccess(ctx)
|
const access = assertTeamAccess(ctx)
|
||||||
const current = await loadQuotationOrThrow(args.quotationUuid, access.teamUuid)
|
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 teamAccess = { teamUuid: current.teamUuid, userId: access.userId }
|
||||||
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||||
await prisma.quotation.update({
|
await prisma.quotation.update({
|
||||||
@@ -1357,7 +1363,8 @@ export const teamResolvers = {
|
|||||||
const locked = await lockTariffPrice(
|
const locked = await lockTariffPrice(
|
||||||
tariff,
|
tariff,
|
||||||
projectedSourceAmount,
|
projectedSourceAmount,
|
||||||
quotationPricingUserId(quotation),
|
quotation.teamUuid,
|
||||||
|
quotation.clientCurrency,
|
||||||
)
|
)
|
||||||
await prisma.quotation.update({
|
await prisma.quotation.update({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Reference in New Issue
Block a user