diff --git a/furniture-3d-system/prisma/migrations/20260819090000_add_product_archive_fee_ratio_operating_profit/migration.sql b/furniture-3d-system/prisma/migrations/20260819090000_add_product_archive_fee_ratio_operating_profit/migration.sql new file mode 100644 index 00000000..d5d12b35 --- /dev/null +++ b/furniture-3d-system/prisma/migrations/20260819090000_add_product_archive_fee_ratio_operating_profit/migration.sql @@ -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); diff --git a/furniture-3d-system/prisma/schema.prisma b/furniture-3d-system/prisma/schema.prisma index 9310f0d2..b7b21995 100644 --- a/furniture-3d-system/prisma/schema.prisma +++ b/furniture-3d-system/prisma/schema.prisma @@ -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]) diff --git a/furniture-3d-system/src/server/repositories/product-archive-sales-performance-source.repository.ts b/furniture-3d-system/src/server/repositories/product-archive-sales-performance-source.repository.ts index 58690209..c16d8a70 100644 --- a/furniture-3d-system/src/server/repositories/product-archive-sales-performance-source.repository.ts +++ b/furniture-3d-system/src/server/repositories/product-archive-sales-performance-source.repository.ts @@ -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 diff --git a/furniture-3d-system/src/server/repositories/supply-chain-product-archive-sales-performance-sync.repository.ts b/furniture-3d-system/src/server/repositories/supply-chain-product-archive-sales-performance-sync.repository.ts index db5f3b88..76559525 100644 --- a/furniture-3d-system/src/server/repositories/supply-chain-product-archive-sales-performance-sync.repository.ts +++ b/furniture-3d-system/src/server/repositories/supply-chain-product-archive-sales-performance-sync.repository.ts @@ -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, })), }) diff --git a/furniture-3d-system/src/server/repositories/supply-chain-product-archive.repository.ts b/furniture-3d-system/src/server/repositories/supply-chain-product-archive.repository.ts index 50553596..d8e65bea 100644 --- a/furniture-3d-system/src/server/repositories/supply-chain-product-archive.repository.ts +++ b/furniture-3d-system/src/server/repositories/supply-chain-product-archive.repository.ts @@ -69,6 +69,8 @@ const productArchiveSelect = { returnQty: true, grossProfit: true, grossMargin: true, + feeRatio: true, + operatingProfit: true, }, }, createdAt: true, diff --git a/furniture-3d-system/src/server/services/__tests__/product-archive-sales-performance.test.ts b/furniture-3d-system/src/server/services/__tests__/product-archive-sales-performance.test.ts index b751b482..5b8c283e 100644 --- a/furniture-3d-system/src/server/services/__tests__/product-archive-sales-performance.test.ts +++ b/furniture-3d-system/src/server/services/__tests__/product-archive-sales-performance.test.ts @@ -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') }) }) diff --git a/furniture-3d-system/src/server/services/__tests__/supply-chain-product-archive-sales-performance-sync.service.test.ts b/furniture-3d-system/src/server/services/__tests__/supply-chain-product-archive-sales-performance-sync.service.test.ts index 9e115b81..c5f03c88 100644 --- a/furniture-3d-system/src/server/services/__tests__/supply-chain-product-archive-sales-performance-sync.service.test.ts +++ b/furniture-3d-system/src/server/services/__tests__/supply-chain-product-archive-sales-performance-sync.service.test.ts @@ -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'), }) diff --git a/furniture-3d-system/src/server/services/product-archive-sales-performance.ts b/furniture-3d-system/src/server/services/product-archive-sales-performance.ts index 92e97a01..778850a6 100644 --- a/furniture-3d-system/src/server/services/product-archive-sales-performance.ts +++ b/furniture-3d-system/src/server/services/product-archive-sales-performance.ts @@ -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), } } diff --git a/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts b/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts index 954f1b47..f0074596 100644 --- a/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts +++ b/furniture-3d-system/src/server/services/supply-chain-product-archive.mapper.ts @@ -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 }