252 lines
7.9 KiB
JavaScript
252 lines
7.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { loadConfig } from '../dist/config.js';
|
|
import {
|
|
CleaningPayoutError,
|
|
CleaningPayoutService
|
|
} from '../dist/cleaning/cleaning-payout-service.js';
|
|
|
|
assert.equal(loadConfig({
|
|
NODE_ENV: 'production',
|
|
QIPAI_MQTT_USERNAME: 'backend-test',
|
|
QIPAI_MQTT_PASSWORD: 'not-a-real-secret',
|
|
QIPAI_JWT_SECRET: 'production-cleaning-payout-secret-long-enough',
|
|
QIPAI_CLEANING_PAYOUT_MOCK_ENABLED: 'true'
|
|
}).payment.cleaningPayoutMockEnabled, false);
|
|
assert.equal(loadConfig({
|
|
NODE_ENV: 'test',
|
|
QIPAI_CLEANING_PAYOUT_MOCK_ENABLED: 'true'
|
|
}).payment.cleaningPayoutMockEnabled, true);
|
|
|
|
const actor = {
|
|
tenantId: '7',
|
|
userId: '21',
|
|
access: { roles: ['TENANT_ADMIN'], capabilities: ['tenant.manage'], storeIds: [] },
|
|
traceId: 'cleaning-payout-test'
|
|
};
|
|
|
|
{
|
|
const harness = createHarness({ transferState: 'SUCCESS' });
|
|
const result = await harness.service.executeWechatTransfer({
|
|
...actor,
|
|
settlementId: '501',
|
|
mode: 'API'
|
|
});
|
|
assert.equal(result.transferState, 'SUCCESS');
|
|
assert.equal(harness.repository.paid[0].payoutChannel, 'WECHAT_TRANSFER');
|
|
assert.equal(harness.state.transferInput.amountCents, 1200);
|
|
assert.equal(harness.state.transferInput.openid, 'openid-cleaner');
|
|
}
|
|
|
|
{
|
|
const harness = createHarness({ transferState: 'WAIT_USER_CONFIRM', packageInfo: 'package-info' });
|
|
const result = await harness.service.executeWechatTransfer({
|
|
...actor,
|
|
settlementId: '501',
|
|
mode: 'API',
|
|
note: 'need user confirm'
|
|
});
|
|
assert.equal(result.transferState, 'WAIT_USER_CONFIRM');
|
|
assert.equal(harness.repository.pending[0].payoutState, 'WAIT_USER_CONFIRM');
|
|
assert.equal(harness.repository.pending[0].payoutPackageInfo, 'package-info');
|
|
assert.equal(harness.repository.paid.length, 0);
|
|
}
|
|
|
|
{
|
|
const harness = createHarness({ queryState: 'SUCCESS' });
|
|
const result = await harness.service.syncWechatTransfer({
|
|
...actor,
|
|
settlementId: '501',
|
|
note: 'poll success'
|
|
});
|
|
assert.equal(result.transferState, 'SUCCESS');
|
|
assert.equal(harness.state.queryOutBillNo, 'CLS-20260627-501');
|
|
assert.equal(harness.repository.paid[0].payoutReference, 'wx-transfer-query-501');
|
|
}
|
|
|
|
{
|
|
const harness = createHarness({ queryState: 'FAIL', queryFailReason: 'ACCOUNT_ABNORMAL' });
|
|
const result = await harness.service.syncWechatTransfer({
|
|
...actor,
|
|
settlementId: '501'
|
|
});
|
|
assert.equal(result.transferState, 'FAIL');
|
|
assert.equal(harness.repository.failures[0].error, 'ACCOUNT_ABNORMAL');
|
|
}
|
|
|
|
{
|
|
const harness = createHarness({ queryState: 'PROCESSING' });
|
|
const result = await harness.service.syncWechatTransfer({
|
|
...actor,
|
|
settlementId: '501'
|
|
});
|
|
assert.equal(result.transferState, 'PROCESSING');
|
|
assert.equal(harness.repository.pending[0].payoutState, 'PROCESSING');
|
|
}
|
|
|
|
{
|
|
const harness = createHarness({
|
|
notificationPayload: {
|
|
mch_id: '1900000109',
|
|
out_bill_no: 'CLS-20260627-501',
|
|
transfer_bill_no: 'wx-notify-501',
|
|
state: 'SUCCESS'
|
|
}
|
|
});
|
|
const result = await harness.service.processWechatTransferNotification({
|
|
timestamp: '1782700000',
|
|
nonce: 'notify-nonce',
|
|
serial: 'PLATFORM-SERIAL',
|
|
signature: 'signature'
|
|
}, '{"resource":"encrypted"}', 'notify-trace');
|
|
assert.equal(result.transferState, 'SUCCESS');
|
|
assert.equal(harness.state.verifiedNotification.rawBody, '{"resource":"encrypted"}');
|
|
assert.equal(harness.repository.paid[0].tenantId, '7');
|
|
assert.equal(harness.repository.paid[0].payoutReference, 'wx-notify-501');
|
|
}
|
|
|
|
{
|
|
const harness = createHarness({ transferState: 'FAIL', failReason: 'REAL_NAME_CHECK_FAILED' });
|
|
const result = await harness.service.executeWechatTransfer({
|
|
...actor,
|
|
settlementId: '501',
|
|
mode: 'API'
|
|
});
|
|
assert.equal(result.transferState, 'FAIL');
|
|
assert.equal(harness.repository.failures[0].error, 'REAL_NAME_CHECK_FAILED');
|
|
}
|
|
|
|
{
|
|
const harness = createHarness({ mockEnabled: true });
|
|
const result = await harness.service.executeWechatTransfer({
|
|
...actor,
|
|
settlementId: '501',
|
|
mode: 'MOCK'
|
|
});
|
|
assert.equal(result.transferState, 'SUCCESS');
|
|
assert.equal(harness.repository.paid[0].payoutChannel, 'WECHAT_TRANSFER_MOCK');
|
|
}
|
|
|
|
{
|
|
const harness = createHarness({ mockEnabled: false });
|
|
await assert.rejects(
|
|
() => harness.service.executeWechatTransfer({
|
|
...actor,
|
|
settlementId: '501',
|
|
mode: 'MOCK'
|
|
}),
|
|
(error) => error instanceof CleaningPayoutError
|
|
&& error.code === 'CLEANING_PAYOUT_MOCK_DISABLED'
|
|
);
|
|
}
|
|
|
|
function createHarness(options = {}) {
|
|
const state = {
|
|
transferInput: null,
|
|
queryOutBillNo: null,
|
|
settlement: {
|
|
id: '501',
|
|
settlementNo: 'CLS-20260627-501',
|
|
cleanerUserId: '31',
|
|
storeId: '11',
|
|
status: options.status ?? 'CONFIRMED',
|
|
totalRewardCents: 1200,
|
|
payoutChannel: options.payoutChannel ?? 'WECHAT_TRANSFER',
|
|
payoutReference: options.payoutReference ?? 'CLS-20260627-501',
|
|
payoutState: options.payoutState ?? 'WAIT_USER_CONFIRM',
|
|
payoutPackageInfo: options.payoutPackageInfo ?? 'package-info'
|
|
},
|
|
account: {
|
|
id: '41',
|
|
platformAppId: '9',
|
|
storeId: '11',
|
|
merchantId: '1900000109',
|
|
credentialRef: 'wechat-cleaning',
|
|
authorizationStatus: options.authorizationStatus ?? 'AUTHORIZED'
|
|
}
|
|
};
|
|
const pool = {
|
|
async execute(sql) {
|
|
if (sql.includes("payout_channel = 'WECHAT_TRANSFER'")) {
|
|
return [[{ ...state.settlement, tenantId: '7' }], []];
|
|
}
|
|
if (sql.includes('FROM qipai_cleaning_settlements')) {
|
|
return [[state.settlement], []];
|
|
}
|
|
if (sql.includes('FROM qipai_collection_accounts')) {
|
|
return [[state.account], []];
|
|
}
|
|
if (sql.includes('FROM qipai_user_identities')) {
|
|
return [[{ openid: 'openid-cleaner' }], []];
|
|
}
|
|
throw new Error(`Unexpected SQL: ${sql}`);
|
|
}
|
|
};
|
|
const repository = {
|
|
paid: [],
|
|
failures: [],
|
|
pending: [],
|
|
async markSettlementPaid(input) {
|
|
this.paid.push(input);
|
|
return { ...state.settlement, status: 'PAID', payoutReference: input.payoutReference };
|
|
},
|
|
async recordSettlementPayoutFailure(input) {
|
|
this.failures.push(input);
|
|
return { ...state.settlement, payoutError: input.error };
|
|
},
|
|
async recordSettlementPayoutPending(input) {
|
|
this.pending.push(input);
|
|
return { ...state.settlement, payoutState: input.payoutState };
|
|
}
|
|
};
|
|
const client = {
|
|
async createMerchantTransfer(_credential, input) {
|
|
state.transferInput = input;
|
|
return {
|
|
outBillNo: input.outBillNo,
|
|
transferBillNo: 'wx-transfer-501',
|
|
state: options.transferState ?? 'SUCCESS',
|
|
failReason: options.failReason ?? '',
|
|
packageInfo: options.packageInfo ?? ''
|
|
};
|
|
},
|
|
async queryMerchantTransferByOutBillNo(_credential, outBillNo) {
|
|
state.queryOutBillNo = outBillNo;
|
|
return {
|
|
merchantId: options.queryMerchantId ?? '1900000109',
|
|
outBillNo,
|
|
transferBillNo: 'wx-transfer-query-501',
|
|
state: options.queryState ?? 'SUCCESS',
|
|
failReason: options.queryFailReason ?? '',
|
|
amountCents: options.queryAmountCents ?? 1200
|
|
};
|
|
},
|
|
verifyAndDecrypt(_credential, headers, rawBody) {
|
|
state.verifiedNotification = { headers, rawBody };
|
|
return options.notificationPayload ?? {};
|
|
}
|
|
};
|
|
const credential = {
|
|
appId: 'wx-test',
|
|
merchantId: '1900000109',
|
|
serialNo: 'serial',
|
|
privateKeyPem: 'private-key',
|
|
apiV3Key: '0123456789abcdef0123456789abcdef',
|
|
platformCertificates: { 'PLATFORM-SERIAL': 'certificate' },
|
|
transferSceneId: '1000',
|
|
transferSceneReportInfos: [{ infoType: '岗位类型', infoContent: '保洁员' }]
|
|
};
|
|
return {
|
|
state,
|
|
repository,
|
|
service: new CleaningPayoutService(
|
|
pool,
|
|
repository,
|
|
client,
|
|
new Map([['wechat-cleaning', credential]]),
|
|
options.mockEnabled ?? false
|
|
)
|
|
};
|
|
}
|
|
|
|
console.log('PASS: M08-B cleaning payout service handles WeChat transfer states and mock gate.');
|