feat(assoc): v4.0.1 合伙人关联码、分佣账单与 H5 用户管理
订单佣金只认关联用户;合伙人备注写入独立表;H5 增加用户管理与首页统计。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -76,6 +76,51 @@ function wineryDayWindow(anchor = new Date(), lagDays = WINERY_SETTLEMENT_LAG_DA
|
||||
return { start, end, billDate: start };
|
||||
}
|
||||
|
||||
function toPartnerBillItemDto(row: {
|
||||
id: bigint;
|
||||
kind: string;
|
||||
refId: bigint;
|
||||
refNo: string;
|
||||
title: string | null;
|
||||
extra: string | null;
|
||||
baseAmount: Prisma.Decimal | number;
|
||||
rate: Prisma.Decimal | number;
|
||||
commission: Prisma.Decimal | number;
|
||||
occurredAt: Date;
|
||||
}) {
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
kind: row.kind,
|
||||
refId: row.refId.toString(),
|
||||
refNo: row.refNo,
|
||||
title: row.title,
|
||||
extra: row.extra,
|
||||
baseAmount: Number(row.baseAmount),
|
||||
rate: Number(row.rate),
|
||||
commission: Number(row.commission),
|
||||
occurredAt: row.occurredAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function splitPartnerBillItems(
|
||||
items: Array<{
|
||||
id: bigint;
|
||||
kind: string;
|
||||
refId: bigint;
|
||||
refNo: string;
|
||||
title: string | null;
|
||||
extra: string | null;
|
||||
baseAmount: Prisma.Decimal | number;
|
||||
rate: Prisma.Decimal | number;
|
||||
commission: Prisma.Decimal | number;
|
||||
occurredAt: Date;
|
||||
}>,
|
||||
) {
|
||||
const orderItems = items.filter((i) => i.kind === 'ORDER').map(toPartnerBillItemDto);
|
||||
const redeemItems = items.filter((i) => i.kind === 'REDEEM').map(toPartnerBillItemDto);
|
||||
return { orderItems, redeemItems };
|
||||
}
|
||||
|
||||
function round2(n: number) {
|
||||
return Math.round(n * 100) / 100;
|
||||
}
|
||||
@@ -1526,6 +1571,7 @@ export class SettlementService implements OnModuleInit {
|
||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||
const bill = await this.prisma.partnerBill.findFirst({
|
||||
where: { id: billId, partnerAccountId: primary.id },
|
||||
include: { items: { orderBy: { occurredAt: 'asc' } } },
|
||||
});
|
||||
if (!bill) throw new NotFoundException('账单不存在');
|
||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||
@@ -1535,7 +1581,12 @@ export class SettlementService implements OnModuleInit {
|
||||
refId: billId,
|
||||
extraJson: { billId: billId.toString(), status: bill.status },
|
||||
});
|
||||
return serializeBigInt(bill);
|
||||
const { items, ...header } = bill;
|
||||
return serializeBigInt({
|
||||
...header,
|
||||
partnerId: header.partnerAccountId.toString(),
|
||||
...splitPartnerBillItems(items),
|
||||
});
|
||||
}
|
||||
|
||||
async listAdminPartnerBills(query: {
|
||||
@@ -1689,10 +1740,15 @@ export class SettlementService implements OnModuleInit {
|
||||
async getAdminPartnerBill(id: bigint) {
|
||||
const bill = await this.prisma.partnerBill.findUnique({
|
||||
where: { id },
|
||||
include: { partnerAccount: true },
|
||||
include: { partnerAccount: true, items: { orderBy: { occurredAt: 'asc' } } },
|
||||
});
|
||||
if (!bill) throw new NotFoundException('账单不存在');
|
||||
return serializeBigInt(bill);
|
||||
const { items, ...header } = bill;
|
||||
return serializeBigInt({
|
||||
...header,
|
||||
partnerId: header.partnerAccountId.toString(),
|
||||
...splitPartnerBillItems(items),
|
||||
});
|
||||
}
|
||||
|
||||
async generatePartnerBill(
|
||||
@@ -1717,30 +1773,39 @@ export class SettlementService implements OnModuleInit {
|
||||
throw new BadRequestException('合伙人未绑定开城城市');
|
||||
}
|
||||
|
||||
const orderCommissionRate = Number(primary.orderCommissionRate ?? 0);
|
||||
const redeemCommissionRate = Number(primary.redeemCommissionRate ?? 0.03);
|
||||
|
||||
const orders = await this.prisma.order.findMany({
|
||||
where: {
|
||||
cityId: primary.cityId,
|
||||
partnerAccountIdAtPay: primary.id,
|
||||
payStatus: 'PAID',
|
||||
paidAt: { gte: periodStart, lte: periodEnd },
|
||||
},
|
||||
orderBy: { paidAt: 'asc' },
|
||||
});
|
||||
const orderCommission = orders.reduce((sum, o) => {
|
||||
if (o.partnerAccountIdAtPay) {
|
||||
if (o.partnerAccountIdAtPay !== primary.id) return sum;
|
||||
const rate = o.orderCommissionRateAtPay != null ? Number(o.orderCommissionRateAtPay) : 0;
|
||||
return sum + Number(o.payAmount) * rate;
|
||||
}
|
||||
return sum + Number(o.payAmount) * orderCommissionRate;
|
||||
}, 0);
|
||||
const orderRows = orders.map((o) => {
|
||||
const rate = o.orderCommissionRateAtPay != null ? Number(o.orderCommissionRateAtPay) : 0;
|
||||
const baseAmount = Number(o.payAmount);
|
||||
return {
|
||||
kind: 'ORDER' as const,
|
||||
refId: o.id,
|
||||
refNo: o.orderNo,
|
||||
title: o.productName,
|
||||
extra: `×${o.quantity}`,
|
||||
baseAmount,
|
||||
rate,
|
||||
commission: round2(baseAmount * rate),
|
||||
occurredAt: o.paidAt ?? o.createdAt,
|
||||
};
|
||||
});
|
||||
const orderCommission = orderRows.reduce((sum, r) => sum + r.commission, 0);
|
||||
|
||||
const stores = await this.prisma.store.findMany({
|
||||
where: { partnerAccountId: primary.id },
|
||||
select: { id: true },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const storeIds = stores.map((s) => s.id);
|
||||
const storeNameById = new Map(stores.map((s) => [s.id.toString(), s.name]));
|
||||
const redeems = !storeIds.length
|
||||
? []
|
||||
: await this.prisma.redeemRecord.findMany({
|
||||
@@ -1748,37 +1813,71 @@ export class SettlementService implements OnModuleInit {
|
||||
storeId: { in: storeIds },
|
||||
createdAt: { gte: periodStart, lte: periodEnd },
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
const redeemCommission = redeems.reduce(
|
||||
(sum, r) => sum + Number(r.amount) * redeemCommissionRate,
|
||||
0,
|
||||
);
|
||||
const redeemRows = redeems.map((r) => {
|
||||
const baseAmount = Number(r.amount);
|
||||
return {
|
||||
kind: 'REDEEM' as const,
|
||||
refId: r.id,
|
||||
refNo: r.redeemNo,
|
||||
title: storeNameById.get(r.storeId.toString()) ?? '门店',
|
||||
extra: null as string | null,
|
||||
baseAmount,
|
||||
rate: redeemCommissionRate,
|
||||
commission: round2(baseAmount * redeemCommissionRate),
|
||||
occurredAt: r.createdAt,
|
||||
};
|
||||
});
|
||||
const redeemCommission = redeemRows.reduce((sum, r) => sum + r.commission, 0);
|
||||
|
||||
const totalAmount = round2(orderCommission + redeemCommission);
|
||||
const itemRows = [...orderRows, ...redeemRows];
|
||||
|
||||
const bill = existing
|
||||
? await this.prisma.partnerBill.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
orderCommission: round2(orderCommission),
|
||||
redeemCommission: round2(redeemCommission),
|
||||
totalAmount,
|
||||
periodEnd,
|
||||
status: 'PENDING_REVIEW',
|
||||
},
|
||||
})
|
||||
: await this.prisma.partnerBill.create({
|
||||
data: {
|
||||
billNo: generateBillNo('PB'),
|
||||
partnerAccountId: primary.id,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
orderCommission: round2(orderCommission),
|
||||
redeemCommission: round2(redeemCommission),
|
||||
totalAmount,
|
||||
status: 'PENDING_REVIEW',
|
||||
},
|
||||
const bill = await this.prisma.$transaction(async (tx) => {
|
||||
const header = existing
|
||||
? await tx.partnerBill.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
orderCommission: round2(orderCommission),
|
||||
redeemCommission: round2(redeemCommission),
|
||||
totalAmount,
|
||||
periodEnd,
|
||||
status: 'PENDING_REVIEW',
|
||||
},
|
||||
})
|
||||
: await tx.partnerBill.create({
|
||||
data: {
|
||||
billNo: generateBillNo('PB'),
|
||||
partnerAccountId: primary.id,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
orderCommission: round2(orderCommission),
|
||||
redeemCommission: round2(redeemCommission),
|
||||
totalAmount,
|
||||
status: 'PENDING_REVIEW',
|
||||
},
|
||||
});
|
||||
|
||||
await tx.partnerBillItem.deleteMany({ where: { partnerBillId: header.id } });
|
||||
if (itemRows.length > 0) {
|
||||
await tx.partnerBillItem.createMany({
|
||||
data: itemRows.map((row) => ({
|
||||
partnerBillId: header.id,
|
||||
kind: row.kind,
|
||||
refId: row.refId,
|
||||
refNo: row.refNo,
|
||||
title: row.title,
|
||||
extra: row.extra,
|
||||
baseAmount: row.baseAmount,
|
||||
rate: new Prisma.Decimal(row.rate.toFixed(4)),
|
||||
commission: row.commission,
|
||||
occurredAt: row.occurredAt,
|
||||
})),
|
||||
});
|
||||
}
|
||||
return header;
|
||||
});
|
||||
|
||||
if (opts?.notify !== false) {
|
||||
let cityName = '—';
|
||||
@@ -1980,6 +2079,7 @@ export class SettlementService implements OnModuleInit {
|
||||
bankBranch: true,
|
||||
},
|
||||
},
|
||||
items: { orderBy: { occurredAt: 'asc' } },
|
||||
},
|
||||
orderBy: { periodStart: 'desc' },
|
||||
});
|
||||
@@ -2024,7 +2124,25 @@ export class SettlementService implements OnModuleInit {
|
||||
csvEscape(b.rejectReason ?? ''),
|
||||
].join(','),
|
||||
);
|
||||
return { csv: `\uFEFF${[header, ...rows].join('\n')}`, count: bills.length };
|
||||
|
||||
const itemHeader = ['账单号', '类型', '单号', '标题', '备注', '基数', '费率', '佣金', '发生时间'].join(',');
|
||||
const itemRows = bills.flatMap((b) =>
|
||||
b.items.map((it) =>
|
||||
[
|
||||
csvEscape(b.billNo),
|
||||
it.kind === 'ORDER' ? '酒订单' : '核销',
|
||||
csvEscape(it.refNo),
|
||||
csvEscape(it.title ?? ''),
|
||||
csvEscape(it.extra ?? ''),
|
||||
Number(it.baseAmount),
|
||||
Number(it.rate),
|
||||
Number(it.commission),
|
||||
it.occurredAt.toISOString().slice(0, 19).replace('T', ' '),
|
||||
].join(','),
|
||||
),
|
||||
);
|
||||
const csv = ['账单汇总', header, ...rows, '', '酒订单/核销明细', itemHeader, ...itemRows].join('\n');
|
||||
return { csv: `\uFEFF${csv}`, count: bills.length };
|
||||
}
|
||||
|
||||
// ─── Winery bills ────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user