feat(supply-chain): add fee ratio and operating profit
This commit is contained in:
parent
8cfd3f8292
commit
6024abe7aa
@ -0,0 +1,4 @@
|
||||
-- Add derived fee ratio and operating profit to the existing sales snapshot.
|
||||
ALTER TABLE "SupplyChainProductArchiveSalesPerformance"
|
||||
ADD COLUMN "feeRatio" DECIMAL(20,8),
|
||||
ADD COLUMN "operatingProfit" DECIMAL(24,4);
|
||||
@ -580,7 +580,7 @@ model SupplyChainProductArchive {
|
||||
@@index([firstSourceCode, createdAt])
|
||||
}
|
||||
|
||||
// 销售宽表按条码汇总的每日快照;费比和营业利润额待完整费用字段开放后再接入。
|
||||
// 销售宽表按条码汇总的每日快照。
|
||||
model SupplyChainProductArchiveSalesPerformance {
|
||||
archiveId String @id
|
||||
archive SupplyChainProductArchive @relation(fields: [archiveId], references: [id], onDelete: Cascade)
|
||||
@ -594,6 +594,8 @@ model SupplyChainProductArchiveSalesPerformance {
|
||||
returnQty BigInt
|
||||
grossProfit Decimal @db.Decimal(24, 4)
|
||||
grossMargin Decimal? @db.Decimal(20, 8)
|
||||
feeRatio Decimal? @db.Decimal(20, 8)
|
||||
operatingProfit Decimal? @db.Decimal(24, 4)
|
||||
syncedAt DateTime @default(now())
|
||||
|
||||
@@index([periodEnd])
|
||||
|
||||
@ -9,6 +9,15 @@ export type ProductArchiveSalesPerformanceAggregate = {
|
||||
returnQty: string
|
||||
clearSalesCost: string
|
||||
sendFreight: string
|
||||
platformCommission: string
|
||||
xyCost: string
|
||||
afterSaleAmount: string
|
||||
pushFeeTotal: string
|
||||
marketingFeeTotal: string
|
||||
advertisementFeeTotal: string
|
||||
sharedWarehousingSum: string
|
||||
otherFee: string
|
||||
operatingFee: string
|
||||
}
|
||||
|
||||
export interface ProductArchiveSalesPerformanceSourceRepository {
|
||||
@ -112,7 +121,16 @@ class ProductArchiveSalesPerformanceSourceRepositoryClass
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(goods_count, 0) AS bigint))) AS shipQty,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(refund_count, 0) AS bigint))) AS returnQty,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(clear_sales_cost, 0) AS decimal(38, 4)))) AS clearSalesCost,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(send_freight, 0) AS decimal(38, 4)))) AS sendFreight
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(send_freight, 0) AS decimal(38, 4)))) AS sendFreight,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(platform_commission, 0) AS decimal(38, 4)))) AS platformCommission,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(xy_cost, 0) AS decimal(38, 4)))) AS xyCost,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(after_sale_amount, 0) AS decimal(38, 4)))) AS afterSaleAmount,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(push_fee_total, 0) AS decimal(38, 4)))) AS pushFeeTotal,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(marketing_fee_total, 0) AS decimal(38, 4)))) AS marketingFeeTotal,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(advertisement_fee_total, 0) AS decimal(38, 4)))) AS advertisementFeeTotal,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(shared_warehousing_sum, 0) AS decimal(38, 4)))) AS sharedWarehousingSum,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(other_fee, 0) AS decimal(38, 4)))) AS otherFee,
|
||||
CONVERT(varchar(64), SUM(CAST(COALESCE(operating_fee, 0) AS decimal(38, 4)))) AS operatingFee
|
||||
FROM ${this.sourceObject()}
|
||||
WHERE stat_date >= @periodStart
|
||||
AND stat_date <= @periodEnd
|
||||
|
||||
@ -71,6 +71,11 @@ class SupplyChainProductArchiveSalesPerformanceSyncRepositoryClass
|
||||
snapshot.grossMargin === null
|
||||
? null
|
||||
: new Prisma.Decimal(snapshot.grossMargin),
|
||||
feeRatio:
|
||||
snapshot.feeRatio === null
|
||||
? null
|
||||
: new Prisma.Decimal(snapshot.feeRatio),
|
||||
operatingProfit: new Prisma.Decimal(snapshot.operatingProfit),
|
||||
syncedAt: snapshot.syncedAt,
|
||||
})),
|
||||
})
|
||||
|
||||
@ -69,6 +69,8 @@ const productArchiveSelect = {
|
||||
returnQty: true,
|
||||
grossProfit: true,
|
||||
grossMargin: true,
|
||||
feeRatio: true,
|
||||
operatingProfit: true,
|
||||
},
|
||||
},
|
||||
createdAt: true,
|
||||
|
||||
@ -11,6 +11,15 @@ describe('calculateProductArchiveSalesPerformance', () => {
|
||||
returnQty: '1',
|
||||
clearSalesCost: '60',
|
||||
sendFreight: '15',
|
||||
platformCommission: '2',
|
||||
xyCost: '3',
|
||||
afterSaleAmount: '4',
|
||||
pushFeeTotal: '5',
|
||||
marketingFeeTotal: '6',
|
||||
advertisementFeeTotal: '7',
|
||||
sharedWarehousingSum: '8',
|
||||
otherFee: '1',
|
||||
operatingFee: '2',
|
||||
})).toEqual({
|
||||
barcode: 'SKU-001',
|
||||
shipAmount: '70.0000',
|
||||
@ -20,6 +29,8 @@ describe('calculateProductArchiveSalesPerformance', () => {
|
||||
returnQty: 1n,
|
||||
grossProfit: '25.0000',
|
||||
grossMargin: '0.25000000',
|
||||
feeRatio: '1.13000000',
|
||||
operatingProfit: '-13.0000',
|
||||
})
|
||||
})
|
||||
|
||||
@ -33,9 +44,20 @@ describe('calculateProductArchiveSalesPerformance', () => {
|
||||
returnQty: '0',
|
||||
clearSalesCost: '10',
|
||||
sendFreight: '2',
|
||||
platformCommission: '0',
|
||||
xyCost: '0',
|
||||
afterSaleAmount: '0',
|
||||
pushFeeTotal: '0',
|
||||
marketingFeeTotal: '0',
|
||||
advertisementFeeTotal: '0',
|
||||
sharedWarehousingSum: '0',
|
||||
otherFee: '0',
|
||||
operatingFee: '0',
|
||||
})
|
||||
|
||||
expect(result.grossProfit).toBe('-12.0000')
|
||||
expect(result.grossMargin).toBeNull()
|
||||
expect(result.feeRatio).toBeNull()
|
||||
expect(result.operatingProfit).toBe('-12.0000')
|
||||
})
|
||||
})
|
||||
|
||||
@ -27,6 +27,15 @@ describe('SupplyChainProductArchiveSalesPerformanceSyncService', () => {
|
||||
returnQty: '1',
|
||||
clearSalesCost: '60',
|
||||
sendFreight: '15',
|
||||
platformCommission: '2',
|
||||
xyCost: '3',
|
||||
afterSaleAmount: '4',
|
||||
pushFeeTotal: '5',
|
||||
marketingFeeTotal: '6',
|
||||
advertisementFeeTotal: '7',
|
||||
sharedWarehousingSum: '8',
|
||||
otherFee: '1',
|
||||
operatingFee: '2',
|
||||
},
|
||||
{
|
||||
barcode: 'NOT-IN-ARCHIVE',
|
||||
@ -37,6 +46,15 @@ describe('SupplyChainProductArchiveSalesPerformanceSyncService', () => {
|
||||
returnQty: '0',
|
||||
clearSalesCost: '10',
|
||||
sendFreight: '2',
|
||||
platformCommission: '0',
|
||||
xyCost: '0',
|
||||
afterSaleAmount: '0',
|
||||
pushFeeTotal: '0',
|
||||
marketingFeeTotal: '0',
|
||||
advertisementFeeTotal: '0',
|
||||
sharedWarehousingSum: '0',
|
||||
otherFee: '0',
|
||||
operatingFee: '0',
|
||||
},
|
||||
]),
|
||||
close: jest.fn().mockResolvedValue(undefined),
|
||||
@ -76,6 +94,8 @@ describe('SupplyChainProductArchiveSalesPerformanceSyncService', () => {
|
||||
barcode: 'SKU-001',
|
||||
grossProfit: '25.0000',
|
||||
grossMargin: '0.25000000',
|
||||
feeRatio: '1.13000000',
|
||||
operatingProfit: '-13.0000',
|
||||
periodStart: new Date('2026-01-01T00:00:00Z'),
|
||||
periodEnd: new Date('2026-08-15T00:00:00Z'),
|
||||
})
|
||||
|
||||
@ -10,6 +10,8 @@ export type ProductArchiveSalesPerformanceValues = {
|
||||
returnQty: bigint
|
||||
grossProfit: string
|
||||
grossMargin: string | null
|
||||
feeRatio: string | null
|
||||
operatingProfit: string
|
||||
}
|
||||
|
||||
export function normalizeSalesPerformanceBarcode(value: string) {
|
||||
@ -43,10 +45,27 @@ export function calculateProductArchiveSalesPerformance(
|
||||
const realPerformance = decimal(aggregate.realPerformance, 'realPerformance')
|
||||
const clearSalesCost = decimal(aggregate.clearSalesCost, 'clearSalesCost')
|
||||
const sendFreight = decimal(aggregate.sendFreight, 'sendFreight')
|
||||
const totalExpense = [
|
||||
sendFreight,
|
||||
clearSalesCost,
|
||||
decimal(aggregate.platformCommission, 'platformCommission'),
|
||||
decimal(aggregate.xyCost, 'xyCost'),
|
||||
decimal(aggregate.afterSaleAmount, 'afterSaleAmount'),
|
||||
decimal(aggregate.pushFeeTotal, 'pushFeeTotal'),
|
||||
decimal(aggregate.marketingFeeTotal, 'marketingFeeTotal'),
|
||||
decimal(aggregate.advertisementFeeTotal, 'advertisementFeeTotal'),
|
||||
decimal(aggregate.sharedWarehousingSum, 'sharedWarehousingSum'),
|
||||
decimal(aggregate.otherFee, 'otherFee'),
|
||||
decimal(aggregate.operatingFee, 'operatingFee'),
|
||||
].reduce((total, value) => total.plus(value), new Prisma.Decimal(0))
|
||||
const grossProfit = realPerformance.minus(clearSalesCost).minus(sendFreight)
|
||||
const grossMargin = realPerformance.isZero()
|
||||
? null
|
||||
: grossProfit.dividedBy(realPerformance).toFixed(8)
|
||||
const feeRatio = realPerformance.isZero()
|
||||
? null
|
||||
: totalExpense.dividedBy(realPerformance).toFixed(8)
|
||||
const operatingProfit = realPerformance.minus(totalExpense)
|
||||
|
||||
return {
|
||||
barcode,
|
||||
@ -57,5 +76,7 @@ export function calculateProductArchiveSalesPerformance(
|
||||
returnQty: integer(aggregate.returnQty, 'returnQty'),
|
||||
grossProfit: grossProfit.toFixed(4),
|
||||
grossMargin,
|
||||
feeRatio,
|
||||
operatingProfit: operatingProfit.toFixed(4),
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,8 +100,8 @@ export function mapProductArchiveRow({
|
||||
shipQty: record.salesPerformance?.shipQty.toString() ?? '',
|
||||
grossProfit: decimalText(record.salesPerformance?.grossProfit ?? null),
|
||||
grossMargin: percentageText(record.salesPerformance?.grossMargin ?? null),
|
||||
feeRatio: '',
|
||||
operatingProfit: '',
|
||||
feeRatio: percentageText(record.salesPerformance?.feeRatio ?? null),
|
||||
operatingProfit: decimalText(record.salesPerformance?.operatingProfit ?? null),
|
||||
returnQty: record.salesPerformance?.returnQty.toString() ?? '',
|
||||
} satisfies SupplyChainProductArchiveRow
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user