feat(M05-C): 完成团购验券与第三方直订
This commit is contained in:
@@ -57,6 +57,9 @@ const paymentVerifySql = read('database/migrations/2026062015_m05a_payment_domai
|
||||
const wechatRefundUpSql = read('database/migrations/2026062216_m05b_wechat_refunds.up.sql');
|
||||
const wechatRefundDownSql = read('database/migrations/2026062216_m05b_wechat_refunds.down.sql');
|
||||
const wechatRefundVerifySql = read('database/migrations/2026062216_m05b_wechat_refunds.verify.sql');
|
||||
const thirdPartyUpSql = read('database/migrations/2026062217_m05c_third_party.up.sql');
|
||||
const thirdPartyDownSql = read('database/migrations/2026062217_m05c_third_party.down.sql');
|
||||
const thirdPartyVerifySql = read('database/migrations/2026062217_m05c_third_party.verify.sql');
|
||||
|
||||
const coreTables = [
|
||||
'qipai_schema_migrations',
|
||||
@@ -252,5 +255,17 @@ assert.match(wechatRefundUpSql, /UNIQUE KEY uq_qipai_refund_client_request/);
|
||||
assert.match(wechatRefundUpSql, /UNIQUE KEY uq_qipai_refund_callback/);
|
||||
assert.match(wechatRefundUpSql, /UNIQUE KEY uq_qipai_reconciliation_request/);
|
||||
assert.doesNotMatch(wechatRefundUpSql, /private_key|api_v3_key|certificate_pem/i);
|
||||
for (const table of [
|
||||
'qipai_third_party_configs', 'qipai_third_party_mappings',
|
||||
'qipai_group_vouchers', 'qipai_group_redemptions', 'qipai_direct_bookings'
|
||||
]) {
|
||||
assert.match(thirdPartyUpSql, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`));
|
||||
assert.match(thirdPartyDownSql, new RegExp(`DROP TABLE IF EXISTS ${table}`));
|
||||
assert.match(thirdPartyVerifySql, new RegExp(`'${table}'`));
|
||||
}
|
||||
assert.match(thirdPartyUpSql, /voucher_hash CHAR\(64\)/);
|
||||
assert.match(thirdPartyUpSql, /UNIQUE KEY uq_qipai_group_redemption_voucher/);
|
||||
assert.match(thirdPartyUpSql, /UNIQUE KEY uq_qipai_direct_booking_event/);
|
||||
assert.doesNotMatch(thirdPartyUpSql, /voucher_code|api_token|webhook_secret/i);
|
||||
|
||||
console.log('PASS: M01-B through M05-B migration contracts are present.');
|
||||
console.log('PASS: M01-B through M05-C migration contracts are present.');
|
||||
|
||||
@@ -27,7 +27,8 @@ assert.match(plan.file, /2026062012_m04b_order_state_machine\.up\.sql/);
|
||||
assert.match(plan.file, /2026062013_m04c_order_adjustments\.up\.sql/);
|
||||
assert.match(plan.file, /2026062014_m04d_order_shares\.up\.sql/);
|
||||
assert.match(plan.file, /2026062015_m05a_payment_domain\.up\.sql/);
|
||||
assert.match(plan.file, /2026062216_m05b_wechat_refunds\.up\.sql$/);
|
||||
assert.match(plan.file, /2026062216_m05b_wechat_refunds\.up\.sql/);
|
||||
assert.match(plan.file, /2026062217_m05c_third_party\.up\.sql$/);
|
||||
assert.match(plan.checksum, /^[a-f0-9]{64}$/);
|
||||
assert.ok(plan.statements.length >= 11);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHmac } from 'node:crypto';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -30,6 +31,8 @@ import {
|
||||
import {
|
||||
PaymentError, PaymentRepository
|
||||
} from '../dist/payments/payment-repository.js';
|
||||
import { ThirdPartyClient } from '../dist/third-party/third-party-client.js';
|
||||
import { ThirdPartyService } from '../dist/third-party/third-party-service.js';
|
||||
import {
|
||||
executeMigrationPlan,
|
||||
loadMigrationPlan,
|
||||
@@ -42,6 +45,9 @@ const expectedTables = [
|
||||
'qipai_audit_logs',
|
||||
'qipai_auth_sessions',
|
||||
'qipai_devices',
|
||||
'qipai_direct_bookings',
|
||||
'qipai_group_redemptions',
|
||||
'qipai_group_vouchers',
|
||||
'qipai_holiday_calendar',
|
||||
'qipai_legacy_table_mappings',
|
||||
'qipai_media_assets',
|
||||
@@ -77,6 +83,8 @@ const expectedTables = [
|
||||
'qipai_tenant_apps',
|
||||
'qipai_tenant_configs',
|
||||
'qipai_tenants',
|
||||
'qipai_third_party_configs',
|
||||
'qipai_third_party_mappings',
|
||||
'qipai_user_admin_profiles',
|
||||
'qipai_user_identities',
|
||||
'qipai_user_roles',
|
||||
@@ -102,12 +110,12 @@ async function readMigrationVersions(pool) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT version, name
|
||||
FROM qipai_schema_migrations
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ORDER BY version`,
|
||||
['2026061601', '2026061802', '2026061803', '2026061804',
|
||||
'2026061805', '2026061806', '2026061807', '2026061808', '2026061809',
|
||||
'2026061810', '2026061811', '2026062012', '2026062013', '2026062014',
|
||||
'2026062015', '2026062216']
|
||||
'2026062015', '2026062216', '2026062217']
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -1152,6 +1160,186 @@ async function assertPaymentDomain(pool, context) {
|
||||
assert.equal(configRows.some((row) => /secret|private.key/i.test(row.settings)), false);
|
||||
}
|
||||
|
||||
async function assertThirdPartyDomain(pool, context) {
|
||||
const [customerRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
INNER JOIN qipai_user_identities i
|
||||
ON i.tenant_id = u.tenant_id AND i.user_id = u.id
|
||||
WHERE u.tenant_id = ? AND i.openid = 'm02b-openid-a' LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const [roomRows] = await pool.query(
|
||||
`SELECT store_id AS storeId, id AS roomId FROM qipai_rooms
|
||||
WHERE tenant_id = ? AND name = 'M04C Target Room' LIMIT 1`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const customerId = String(customerRows[0].id);
|
||||
const storeId = String(roomRows[0].storeId);
|
||||
const roomId = String(roomRows[0].roomId);
|
||||
await pool.query(
|
||||
`INSERT INTO qipai_third_party_configs
|
||||
(tenant_id, store_id, provider, mode, credential_ref, settings)
|
||||
VALUES (?, NULL, 'MEITUAN', 'MOCK', 'env:TP_TEST', JSON_OBJECT())`,
|
||||
[context.tenantId]
|
||||
);
|
||||
const pricing = new PricingRepository(pool);
|
||||
const service = new ThirdPartyService(
|
||||
pool,
|
||||
pricing,
|
||||
new ThirdPartyClient({
|
||||
async request() {
|
||||
throw new Error('Live M05-C test must not call an external provider.');
|
||||
}
|
||||
}),
|
||||
new Map([['TP_TEST', { webhookSecret: 'm05c-webhook-secret' }]])
|
||||
);
|
||||
const startAt = new Date(Date.now() + 27 * 86400000);
|
||||
startAt.setUTCHours(2, 0, 0, 0);
|
||||
const endAt = new Date(startAt.getTime() + 2 * 3600000);
|
||||
const order = await pricing.reserve({
|
||||
tenantId: context.tenantId,
|
||||
userId: customerId,
|
||||
roomId,
|
||||
startAt,
|
||||
endAt,
|
||||
pricingMode: 'HOURLY'
|
||||
});
|
||||
const redeemed = await service.redeemVoucher({
|
||||
tenantId: context.tenantId,
|
||||
userId: customerId,
|
||||
provider: 'MEITUAN',
|
||||
voucherCode: 'M05C-SENSITIVE-VOUCHER-001',
|
||||
orderId: order.orderId,
|
||||
clientRequestId: 'm05c-redeem-request-001'
|
||||
});
|
||||
assert.equal(redeemed.status, 'SUCCEEDED');
|
||||
const duplicate = await service.redeemVoucher({
|
||||
tenantId: context.tenantId,
|
||||
userId: customerId,
|
||||
provider: 'MEITUAN',
|
||||
voucherCode: 'M05C-SENSITIVE-VOUCHER-001',
|
||||
orderId: order.orderId,
|
||||
clientRequestId: 'm05c-redeem-request-001'
|
||||
});
|
||||
assert.equal(duplicate.idempotent, true);
|
||||
const [voucherRows] = await pool.query(
|
||||
`SELECT voucher_hash AS voucherHash, voucher_masked AS voucherMasked,
|
||||
(SELECT COUNT(*) FROM qipai_group_redemptions r
|
||||
WHERE r.voucher_id = v.id) AS redemptionCount
|
||||
FROM qipai_group_vouchers v WHERE tenant_id = ?`,
|
||||
[context.tenantId]
|
||||
);
|
||||
assert.match(voucherRows[0].voucherHash, /^[a-f0-9]{64}$/);
|
||||
assert.equal(voucherRows[0].voucherMasked.includes('SENSITIVE'), false);
|
||||
assert.equal(Number(voucherRows[0].redemptionCount), 1);
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO qipai_third_party_mappings
|
||||
(tenant_id, provider, resource_type, external_ref, local_resource_id)
|
||||
VALUES (?, 'MEITUAN', 'STORE', 'external-store-001', ?),
|
||||
(?, 'MEITUAN', 'ROOM', 'external-room-001', ?)`,
|
||||
[context.tenantId, storeId, context.tenantId, roomId]
|
||||
);
|
||||
const bookingStart = new Date(Date.now() + 29 * 86400000);
|
||||
bookingStart.setUTCHours(2, 0, 0, 0);
|
||||
const bookingEnd = new Date(bookingStart.getTime() + 2 * 3600000);
|
||||
const quote = await pricing.quote({
|
||||
tenantId: context.tenantId,
|
||||
roomId,
|
||||
startAt: bookingStart,
|
||||
endAt: bookingEnd,
|
||||
pricingMode: 'HOURLY'
|
||||
});
|
||||
const payload = {
|
||||
eventId: 'm05c-booking-event-001',
|
||||
externalBookingNo: 'm05c-booking-001',
|
||||
externalStoreRef: 'external-store-001',
|
||||
externalRoomRef: 'external-room-001',
|
||||
customerRef: 'private-customer-ref',
|
||||
startsAt: bookingStart.toISOString(),
|
||||
endsAt: bookingEnd.toISOString(),
|
||||
amountCents: quote.totalCents
|
||||
};
|
||||
const rawBody = JSON.stringify(payload);
|
||||
const signature = createHmac('sha256', 'm05c-webhook-secret')
|
||||
.update(rawBody).digest('hex');
|
||||
const received = await service.receiveDirectBooking({
|
||||
tenantId: context.tenantId,
|
||||
provider: 'MEITUAN',
|
||||
signature,
|
||||
rawBody,
|
||||
eventId: payload.eventId,
|
||||
externalBookingNo: payload.externalBookingNo,
|
||||
externalStoreRef: payload.externalStoreRef,
|
||||
externalRoomRef: payload.externalRoomRef,
|
||||
customerRef: payload.customerRef,
|
||||
startsAt: bookingStart,
|
||||
endsAt: bookingEnd,
|
||||
amountCents: payload.amountCents,
|
||||
payload
|
||||
});
|
||||
assert.equal(received.status, 'READY_TO_CLAIM');
|
||||
assert.equal((await service.receiveDirectBooking({
|
||||
tenantId: context.tenantId,
|
||||
provider: 'MEITUAN',
|
||||
signature,
|
||||
rawBody,
|
||||
eventId: payload.eventId,
|
||||
externalBookingNo: payload.externalBookingNo,
|
||||
externalStoreRef: payload.externalStoreRef,
|
||||
externalRoomRef: payload.externalRoomRef,
|
||||
customerRef: payload.customerRef,
|
||||
startsAt: bookingStart,
|
||||
endsAt: bookingEnd,
|
||||
amountCents: payload.amountCents,
|
||||
payload
|
||||
})).idempotent, true);
|
||||
const claimed = await service.claimDirectBooking({
|
||||
tenantId: context.tenantId,
|
||||
userId: customerId,
|
||||
bookingId: received.bookingId
|
||||
});
|
||||
const [claimedRows] = await pool.query(
|
||||
`SELECT b.status, b.customer_ref_hash AS customerRefHash,
|
||||
o.status AS orderStatus, o.paid_amount_cents AS paidAmountCents,
|
||||
o.total_amount_cents AS totalAmountCents
|
||||
FROM qipai_direct_bookings b
|
||||
INNER JOIN qipai_orders o ON o.id = b.order_id AND o.tenant_id = b.tenant_id
|
||||
WHERE b.id = ?`,
|
||||
[received.bookingId]
|
||||
);
|
||||
assert.equal(claimedRows[0].status, 'CLAIMED');
|
||||
assert.match(claimedRows[0].customerRefHash, /^[a-f0-9]{64}$/);
|
||||
assert.equal(claimedRows[0].orderStatus, 'PAID');
|
||||
assert.equal(claimedRows[0].paidAmountCents, claimedRows[0].totalAmountCents);
|
||||
assert.match(claimed.orderId, /^[1-9]\d*$/);
|
||||
|
||||
const unmappedPayload = {
|
||||
...payload,
|
||||
eventId: 'm05c-booking-event-unmapped',
|
||||
externalBookingNo: 'm05c-booking-unmapped',
|
||||
externalRoomRef: 'missing-room'
|
||||
};
|
||||
const unmappedBody = JSON.stringify(unmappedPayload);
|
||||
const unmapped = await service.receiveDirectBooking({
|
||||
tenantId: context.tenantId,
|
||||
provider: 'MEITUAN',
|
||||
signature: createHmac('sha256', 'm05c-webhook-secret')
|
||||
.update(unmappedBody).digest('hex'),
|
||||
rawBody: unmappedBody,
|
||||
eventId: unmappedPayload.eventId,
|
||||
externalBookingNo: unmappedPayload.externalBookingNo,
|
||||
externalStoreRef: unmappedPayload.externalStoreRef,
|
||||
externalRoomRef: unmappedPayload.externalRoomRef,
|
||||
customerRef: unmappedPayload.customerRef,
|
||||
startsAt: bookingStart,
|
||||
endsAt: bookingEnd,
|
||||
amountCents: unmappedPayload.amountCents,
|
||||
payload: unmappedPayload
|
||||
});
|
||||
assert.equal(unmapped.status, 'PENDING_MAPPING');
|
||||
}
|
||||
|
||||
async function assertContentManagement(pool, context) {
|
||||
const [adminRows] = await pool.query(
|
||||
`SELECT u.id FROM qipai_users u
|
||||
@@ -1255,7 +1443,8 @@ try {
|
||||
{ version: '2026062013', name: 'm04c_order_adjustments' },
|
||||
{ version: '2026062014', name: 'm04d_order_shares' },
|
||||
{ version: '2026062015', name: 'm05a_payment_domain' },
|
||||
{ version: '2026062216', name: 'm05b_wechat_refunds' }
|
||||
{ version: '2026062216', name: 'm05b_wechat_refunds' },
|
||||
{ version: '2026062217', name: 'm05c_third_party' }
|
||||
]);
|
||||
await assertTaskDurability(pool);
|
||||
const loginContext = await assertPlatformTenantIsolation(pool);
|
||||
@@ -1270,13 +1459,14 @@ try {
|
||||
await assertOrderAdjustments(pool, loginContext);
|
||||
await assertOrderShares(pool, loginContext);
|
||||
await assertPaymentDomain(pool, loginContext);
|
||||
await assertThirdPartyDomain(pool, loginContext);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: first up, verify, tenant isolation and revocable auth checks completed.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.down);
|
||||
assert.deepEqual(await readCoreTables(pool), []);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: down removed all M01-B through M05-B tables.');
|
||||
console.log('PASS: down removed all M01-B through M05-C tables.');
|
||||
|
||||
await executeMigrationPlan(pool, plans.up);
|
||||
await executeMigrationPlan(pool, plans.verify);
|
||||
@@ -1297,7 +1487,8 @@ try {
|
||||
{ version: '2026062013', name: 'm04c_order_adjustments' },
|
||||
{ version: '2026062014', name: 'm04d_order_shares' },
|
||||
{ version: '2026062015', name: 'm05a_payment_domain' },
|
||||
{ version: '2026062216', name: 'm05b_wechat_refunds' }
|
||||
{ version: '2026062216', name: 'm05b_wechat_refunds' },
|
||||
{ version: '2026062217', name: 'm05c_third_party' }
|
||||
]);
|
||||
await assertLegacyCompatibility(pool);
|
||||
console.log('PASS: second up and verify restored the schema.');
|
||||
@@ -1380,6 +1571,11 @@ try {
|
||||
'test adapter explicit non-production gate',
|
||||
'Wechat refund idempotency and callback indexes',
|
||||
'Wechat reconciliation request history'
|
||||
,
|
||||
'group voucher hash-only storage and single redemption',
|
||||
'third-party booking webhook idempotency',
|
||||
'mapped booking claim creates a paid order',
|
||||
'unmapped booking enters manual queue'
|
||||
]
|
||||
}, null, 2));
|
||||
} finally {
|
||||
|
||||
@@ -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