fix(settlement): 全部门店可未出账提现并企微提醒
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -66,7 +66,7 @@ export const WECOM_HANDBOOK_ENTRIES: HandbookEntry[] = [
|
||||
body: [
|
||||
'门店账单:核销×60%,T+1 出账;HQ 确认打款。',
|
||||
'合伙人月账独立确认打款。',
|
||||
'未出账提现受白名单/单日上限等 FIN 护栏。',
|
||||
'门店可对未出账核销主动提现(受单日上限);申请后锁定明细不进入次日出账,HQ 审后打款并企微提醒。',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -49,22 +49,13 @@ export class SettlementScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
/** FIN-003:工作日 18:05 扫描超时未审门店提现 */
|
||||
/** FIN-003:工作日 18:05 扫描超时未审门店提现(企微提醒在 SettlementService 内发送) */
|
||||
@Cron('5 18 * * 1-5', { timeZone: 'Asia/Shanghai' })
|
||||
async handleWithdrawOverdueAlert() {
|
||||
this.logger.log('Store withdraw overdue scan start');
|
||||
try {
|
||||
const summary = await this.settlementService.scanOverdueStoreWithdrawals();
|
||||
this.logger.log(`Store withdraw overdue: ${JSON.stringify(summary)}`);
|
||||
if (summary.overdueCount > 0) {
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'finance',
|
||||
title: '门店提现超时未审',
|
||||
detail: `待审 ${summary.pendingCount} 笔,超时 ${summary.overdueCount} 笔,超时金额 ¥${summary.overdueAmount.toFixed(2)}`,
|
||||
dedupeKey: `job_store_withdraw_overdue|${new Date().toISOString().slice(0, 10)}`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error('Store withdraw overdue scan failed', e instanceof Error ? e.stack : e);
|
||||
this.alert.notify({
|
||||
@@ -72,7 +63,7 @@ export class SettlementScheduler {
|
||||
category: 'job',
|
||||
title: '门店提现超时扫描失败',
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
dedupeKey: `job_store_withdraw_overdue|${new Date().toISOString().slice(0, 10)}`,
|
||||
dedupeKey: `job_store_withdraw_overdue_fail|${new Date().toISOString().slice(0, 10)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,9 +357,6 @@ export class AdminStoresService {
|
||||
...(dto.visibilityWhitelistEnabled !== undefined
|
||||
? { visibilityWhitelistEnabled: !!dto.visibilityWhitelistEnabled }
|
||||
: {}),
|
||||
...(dto.withdrawWhitelistEnabled !== undefined
|
||||
? { withdrawWhitelistEnabled: !!dto.withdrawWhitelistEnabled }
|
||||
: {}),
|
||||
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
||||
},
|
||||
});
|
||||
@@ -519,7 +516,6 @@ export class AdminStoresService {
|
||||
openTime2: openTime2 || null,
|
||||
closeTime2: closeTime2 || null,
|
||||
visibilityWhitelistEnabled: whitelistEnabled,
|
||||
withdrawWhitelistEnabled: !!dto.withdrawWhitelistEnabled,
|
||||
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
||||
status: 'OPEN',
|
||||
auditStatus: 'APPROVED',
|
||||
|
||||
@@ -138,11 +138,6 @@ export class CreateStoreDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
visibilityPhones?: string[];
|
||||
|
||||
/** FIN-001:允许未出账手动提现 */
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
withdrawWhitelistEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateStoreDto {
|
||||
@@ -242,11 +237,6 @@ export class UpdateStoreDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
visibilityPhones?: string[];
|
||||
|
||||
/** FIN-001:允许未出账手动提现 */
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
withdrawWhitelistEnabled?: boolean;
|
||||
}
|
||||
|
||||
export class CreateStoreAccountDto {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
import { AlertService } from '../../common/alert/alert.service';
|
||||
import { AnalyticsService } from '../analytics/analytics.service';
|
||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
||||
@@ -72,6 +73,7 @@ export class SettlementService {
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly partnerCityService: PartnerCityService,
|
||||
private readonly fulfillmentProviderService: FulfillmentProviderService,
|
||||
private readonly alert: AlertService,
|
||||
) {}
|
||||
|
||||
// ─── Store payout (line) ─────────────────────────────
|
||||
@@ -168,11 +170,7 @@ export class SettlementService {
|
||||
|
||||
async getShopWithdrawSummary(storeAccountId: bigint, storeId: bigint) {
|
||||
await this.assertShopStoreAccess(storeAccountId, storeId);
|
||||
const [store, account, available, pending, todayApplied] = await Promise.all([
|
||||
this.prisma.store.findUniqueOrThrow({
|
||||
where: { id: storeId },
|
||||
select: { withdrawWhitelistEnabled: true },
|
||||
}),
|
||||
const [account, available, pending, todayApplied] = await Promise.all([
|
||||
this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
select: {
|
||||
@@ -205,7 +203,6 @@ export class SettlementService {
|
||||
todayAppliedAmount: todayApplied,
|
||||
dailyLimit,
|
||||
remainingDailyLimit: Math.max(0, round2(dailyLimit - todayApplied)),
|
||||
whitelistEnabled: store.withdrawWhitelistEnabled,
|
||||
isPrimary: account.isPrimary === 1,
|
||||
hasBankAccount,
|
||||
hasPendingRequest: !!pending,
|
||||
@@ -227,7 +224,7 @@ export class SettlementService {
|
||||
const [store, account, available, pending, todayApplied] = await Promise.all([
|
||||
this.prisma.store.findUniqueOrThrow({
|
||||
where: { id: storeId },
|
||||
select: { withdrawWhitelistEnabled: true },
|
||||
select: { id: true, name: true, phone: true, cityName: true },
|
||||
}),
|
||||
this.prisma.storeAccount.findUniqueOrThrow({
|
||||
where: { id: storeAccountId },
|
||||
@@ -263,7 +260,6 @@ export class SettlementService {
|
||||
: availableAmount;
|
||||
|
||||
const guard = validateStoreWithdraw({
|
||||
whitelistEnabled: store.withdrawWhitelistEnabled,
|
||||
availableAmount,
|
||||
requestAmount,
|
||||
todayApplied,
|
||||
@@ -334,6 +330,21 @@ export class SettlementService {
|
||||
},
|
||||
});
|
||||
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'finance',
|
||||
title: '门店提现待审',
|
||||
detail: [
|
||||
`门店:${store.name}(${store.cityName || '-'} / ${store.phone || '-'})`,
|
||||
`单号:${created.withdrawNo}`,
|
||||
`金额:¥${Number(created.amount).toFixed(2)}`,
|
||||
`明细:${created.payoutCount} 笔未出账核销(已锁定,不进入次日 T+1 出账)`,
|
||||
'请尽快在 HQ「财务 → 门店提现审」处理。',
|
||||
].join('\n'),
|
||||
dedupeKey: `store_withdraw_applied|${created.id.toString()}`,
|
||||
dedupeTtlSec: 3600,
|
||||
});
|
||||
|
||||
return serializeBigInt(created);
|
||||
}
|
||||
|
||||
@@ -433,7 +444,6 @@ export class SettlementService {
|
||||
name: true,
|
||||
cityName: true,
|
||||
phone: true,
|
||||
withdrawWhitelistEnabled: true,
|
||||
},
|
||||
},
|
||||
storeAccount: {
|
||||
@@ -562,6 +572,34 @@ export class SettlementService {
|
||||
/** FIN-003:工作日 18:00 扫描超时未审提现 */
|
||||
async scanOverdueStoreWithdrawals() {
|
||||
const summary = await this.getStoreWithdrawOverdueSummary();
|
||||
if (summary.overdueCount > 0) {
|
||||
const overdueRows = await this.prisma.storeWithdrawRequest.findMany({
|
||||
where: { status: 'PENDING_REVIEW' },
|
||||
include: { store: { select: { name: true, phone: true } } },
|
||||
orderBy: { appliedAt: 'asc' },
|
||||
take: 20,
|
||||
});
|
||||
const now = new Date();
|
||||
const lines = overdueRows
|
||||
.filter((r) => isWithdrawOverdue(r.appliedAt, now))
|
||||
.slice(0, 10)
|
||||
.map(
|
||||
(r) =>
|
||||
`- ${r.store?.name || r.storeId} ${r.withdrawNo} ¥${Number(r.amount).toFixed(2)}`,
|
||||
);
|
||||
this.alert.notify({
|
||||
level: 'P1',
|
||||
category: 'finance',
|
||||
title: '门店提现超时未审',
|
||||
detail: [
|
||||
`待审 ${summary.pendingCount} 笔,超时 ${summary.overdueCount} 笔,超时金额 ¥${summary.overdueAmount.toFixed(2)}`,
|
||||
...lines,
|
||||
'请尽快在 HQ「财务 → 门店提现审」处理。',
|
||||
].join('\n'),
|
||||
dedupeKey: `store_withdraw_overdue|${new Date().toISOString().slice(0, 10)}`,
|
||||
dedupeTtlSec: 6 * 3600,
|
||||
});
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user