feat(M05-C): 完成团购验券与第三方直订
This commit is contained in:
@@ -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.');
|
||||
Reference in New Issue
Block a user