feat(M05-C): 完成团购验券与第三方直订

This commit is contained in:
Codex
2026-06-22 11:28:50 +08:00
parent 3ec74bb751
commit cda640baa0
17 changed files with 1654 additions and 13 deletions
+156
View File
@@ -0,0 +1,156 @@
import assert from 'node:assert/strict';
import { createHmac } from 'node:crypto';
import { buildApp } from '../dist/app.js';
import { signAccessToken } from '../dist/auth/jwt.js';
import {
ThirdPartyClient, ThirdPartyError
} from '../dist/third-party/third-party-client.js';
const client = new ThirdPartyClient({
async request() {
return {
status: 200,
body: JSON.stringify({
accepted: true,
amountCents: 3600,
productId: 'product-sanitized',
voucherCode: 'must-be-redacted'
})
};
}
});
const body = JSON.stringify({ eventId: 'event-001', amountCents: 3600 });
const signature = createHmac('sha256', 'test-webhook-secret')
.update(body).digest('hex');
client.verifyWebhook('test-webhook-secret', body, signature);
assert.throws(
() => client.verifyWebhook('test-webhook-secret', `${body} `, signature),
(error) => error instanceof ThirdPartyError
&& error.code === 'THIRD_PARTY_SIGNATURE_INVALID'
);
assert.equal((await client.redeemVoucher({
mode: 'MOCK',
provider: 'MEITUAN',
voucherCode: 'MOCK-ACCEPT',
orderNo: 'QP001',
expectedAmountCents: 3600,
settings: {}
})).status, 'SUCCEEDED');
assert.equal((await client.redeemVoucher({
mode: 'MOCK',
provider: 'DOUYIN',
voucherCode: 'FAIL-REJECT',
orderNo: 'QP002',
expectedAmountCents: 3600,
settings: {}
})).status, 'FAILED');
const apiResult = await client.redeemVoucher({
mode: 'API',
provider: 'MEITUAN',
voucherCode: 'API-ACCEPT',
orderNo: 'QP003',
expectedAmountCents: 3600,
settings: { redeemEndpoint: 'https://partner.example.test/redeem' },
credential: { apiToken: 'test-token' }
});
assert.equal(apiResult.amountCents, 3600);
assert.equal('voucherCode' in apiResult.response, false);
const secret = 'third-party-route-secret-with-32-characters';
const token = signAccessToken({
sub: '21', sid: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
tid: '7', aid: '9', rv: 1
}, secret, 900);
let redeemInput;
let notifyInput;
const routeApp = await buildApp({
thirdParty: {
jwtSecret: secret,
authRepository: {
async validateSession() {
return {
id: '5c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
tenantId: '7',
platformAppId: '9',
expiresAt: new Date(Date.now() + 60000),
user: {
id: '21', tenantId: '7', userType: 'CUSTOMER', status: 'ACTIVE',
roleVersion: 1, nickname: '', avatarUrl: '', phone: ''
}
};
}
},
accessControl: {
async getAccessProfile() {
return {
roles: ['TENANT_ADMIN'],
capabilities: ['tenant.manage'],
storeIds: []
};
}
},
service: {
async redeemVoucher(input) {
redeemInput = input;
return { redemptionId: '51', status: 'SUCCEEDED' };
},
async redeemVoucherManually() {
return { redemptionId: '52', status: 'SUCCEEDED' };
},
async receiveDirectBooking(input) {
notifyInput = input;
return { bookingId: '61', status: 'PENDING_MAPPING' };
},
async claimDirectBooking() {
return { bookingId: '61', orderId: '71' };
},
async listRecords() {
return { bookings: [], redemptions: [] };
},
async saveConfig() {
return { configId: '81', created: true };
},
async saveMapping() {
return { mapped: true };
}
}
}
});
const redeemed = await routeApp.inject({
method: 'POST',
url: '/app-api/group-vouchers/redeem',
headers: { authorization: `Bearer ${token}` },
payload: {
provider: 'MEITUAN',
voucherCode: '1234567890',
orderId: '31',
clientRequestId: 'redeem-request-001'
}
});
assert.equal(redeemed.statusCode, 200);
assert.equal(redeemInput.userId, '21');
const bookingPayload = {
eventId: 'event-001',
externalBookingNo: 'booking-001',
externalStoreRef: 'store-external',
externalRoomRef: 'room-external',
customerRef: 'customer-private',
startsAt: new Date(Date.now() + 86400000).toISOString(),
endsAt: new Date(Date.now() + 90000000).toISOString(),
amountCents: 3600
};
const notified = await routeApp.inject({
method: 'POST',
url: '/app-api/third-party/MEITUAN/tenants/7/bookings/notify',
headers: { 'x-third-party-signature': 'signature-sanitized' },
payload: bookingPayload
});
assert.equal(notified.statusCode, 200);
assert.equal(notifyInput.tenantId, '7');
assert.equal(notifyInput.provider, 'MEITUAN');
assert.equal(notifyInput.rawBody, JSON.stringify(bookingPayload));
await routeApp.close();
console.log('PASS: M05-C adapters, webhook authentication and third-party routes.');