import { loginWithSms } from './sms-test-helper.mjs'; const API = process.env.SMOKE_API ?? 'http://localhost:3000/api/v1'; async function req(clientApp, path, options = {}) { const headers = { 'Content-Type': 'application/json', 'X-Client-App': clientApp, ...(options.token ? { Authorization: `Bearer ${options.token}` } : {}), }; const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body }); const json = await res.json(); if (json.code !== 0) throw new Error(`${path}: ${json.message}`); return json.data; } async function expectFail(clientApp, path, options = {}) { const headers = { 'Content-Type': 'application/json', 'X-Client-App': clientApp, ...(options.token ? { Authorization: `Bearer ${options.token}` } : {}), }; const res = await fetch(`${API}${path}`, { ...options, headers, body: options.body }); const json = await res.json(); if (json.code === 0) throw new Error(`${path}: expected failure`); return json.message; } async function sendSms(clientApp, phone, scene, sendPath = '/auth/sms/send') { await req(clientApp, sendPath, { method: 'POST', body: JSON.stringify({ phone, scene }), }); } async function loginSms(clientApp, phone, scene, loginPath = '/auth/login/sms', sendPath = '/auth/sms/send') { return loginWithSms(clientApp, phone, scene, loginPath, sendPath); } async function adminLogin() { return loginSms( 'HQ_WEB', '13600000001', 'HQ_LOGIN', '/admin/auth/login/sms', '/admin/auth/sms/send', ); } async function main() { console.log('1. Health'); await req('USER_H5', '/health'); console.log('2. User login'); const userLogin = await loginSms('USER_H5', '13800000001', 'USER_LOGIN'); const userToken = userLogin.accessToken; console.log('3. Cities + products'); const cities = await req('USER_H5', '/catalog/cities'); const cityCode = cities[0]?.code ?? 'ZZ'; const products = await req('USER_H5', `/catalog/products?cityCode=${cityCode}`); if (products.length < 4) throw new Error(`Expected >=4 products, got ${products.length}`); console.log('4. Min purchase boundaries'); const addr = await req('USER_H5', '/user/addresses', { method: 'POST', token: userToken, body: JSON.stringify({ receiverName: '测试', phone: '13800000001', province: '河南省', city: '郑州市', district: '金水区', detail: 'V3冒烟地址', isDefault: true, }), }); await expectFail('USER_H5', '/trade/orders/preview', { method: 'POST', token: userToken, body: JSON.stringify({ productId: products[0].id, quantity: 1, addressId: addr.id }), }); const preview2 = await req('USER_H5', '/trade/orders/preview', { method: 'POST', token: userToken, body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }), }); if (preview2.payAmount <= 0) throw new Error('Preview qty=2 failed'); console.log('5. Create order + pay + benefit'); const order = await req('USER_H5', '/trade/orders', { method: 'POST', token: userToken, body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }), }); await req('USER_H5', `/trade/orders/${order.id}/pay`, { method: 'POST', token: userToken }); const paidOrder = await req('USER_H5', `/trade/orders/${order.id}`, { token: userToken }); if (paidOrder.status !== 'PENDING_SHIP') throw new Error('Order not PENDING_SHIP after pay'); const coupons = await req('USER_H5', '/benefit/coupons', { token: userToken }); if (!coupons.length) throw new Error('No coupon after pay'); const summary = await req('USER_H5', '/benefit/summary', { token: userToken }); console.log('6. Direct redeem (no couponId)'); const directAmount = Math.min(100, Number(summary.totalBalance ?? summary.balance ?? 100)); const directToken = await req('USER_H5', '/redeem/tokens', { method: 'POST', token: userToken, body: JSON.stringify({ amount: directAmount }), }); if (!directToken.token) throw new Error('Direct redeem token missing'); console.log('7. Coupon redeem cap'); await expectFail('USER_H5', '/redeem/tokens', { method: 'POST', token: userToken, body: JSON.stringify({ couponId: coupons[0].id, amount: Number(coupons[0].balance) + 1 }), }); console.log('7b. Shop sms guard + session'); const unboundMsg = await expectFail('SHOP_H5', '/shop/auth/sms/send', { method: 'POST', body: JSON.stringify({ phone: '13899999999', scene: 'STORE_LOGIN' }), }); if (!unboundMsg.includes('未绑定门店')) { throw new Error(`Expected unbound store message, got: ${unboundMsg}`); } console.log('8. Shop confirm redeem'); const shopLogin = await loginSms( 'SHOP_H5', '13910000001', 'STORE_LOGIN', '/shop/auth/login/sms', '/shop/auth/sms/send', ); let shopToken = shopLogin.accessToken; let shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken }); if (!shopMe?.phone) throw new Error('Shop /shop/auth/me failed'); const refreshed = await req('SHOP_H5', '/shop/auth/token/refresh', { method: 'POST', body: JSON.stringify({ refreshToken: shopLogin.refreshToken }), }); if (!refreshed?.accessToken) throw new Error('Shop token refresh failed'); shopToken = refreshed.accessToken; shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken }); if (!shopMe.storeId && shopMe.stores?.[0]?.storeId) { const selected = await req('SHOP_H5', '/shop/auth/select-store', { method: 'POST', token: shopToken, body: JSON.stringify({ storeId: shopMe.stores[0].storeId }), }); shopToken = selected.accessToken || shopToken; shopMe = await req('SHOP_H5', '/shop/auth/me', { token: shopToken }); } if (!shopMe.storeId) throw new Error('Shop storeId missing after select-store'); const preview = await req('SHOP_H5', '/shop/redeem/preview', { method: 'POST', token: shopToken, body: JSON.stringify({ token: directToken.token }), }); if (!preview.amount) throw new Error('Redeem preview failed'); await req('SHOP_H5', '/shop/redeem/confirm', { method: 'POST', token: shopToken, body: JSON.stringify({ token: directToken.token }), }); console.log('9. Store withdraw (OPT-010)'); const admin = await adminLogin(); const shopStoreId = shopMe.storeId || shopMe.stores?.[0]?.storeId; if (!shopStoreId) throw new Error('Shop storeId missing after login'); const withdrawSummary = await req('SHOP_H5', '/shop/withdraw/summary', { token: shopToken, }); if (!(withdrawSummary.availableAmount > 0)) { throw new Error('Expected available withdraw amount after redeem'); } const applied = await req('SHOP_H5', '/shop/withdraw', { method: 'POST', token: shopToken, body: JSON.stringify({}), }); if (applied.status !== 'PENDING_REVIEW') { throw new Error(`Expected PENDING_REVIEW withdraw, got ${applied.status}`); } const pendingDup = await expectFail('SHOP_H5', '/shop/withdraw', { method: 'POST', token: shopToken, body: JSON.stringify({}), }); if (!String(pendingDup).includes('待审核')) { throw new Error(`Expected pending-request reject, got: ${pendingDup}`); } // 锁定中的明细不可再出账(T+1 排除 withdrawItem) const summaryAfter = await req('SHOP_H5', '/shop/withdraw/summary', { token: shopToken, }); if (summaryAfter.availableAmount !== 0) { throw new Error('Expected availableAmount=0 while withdraw pending'); } await req('HQ_WEB', `/admin/store-withdrawals/${applied.id}/approve`, { method: 'POST', token: admin.accessToken, body: JSON.stringify({ paymentRef: 'smoke-withdraw' }), }); const withdrawList = await req('SHOP_H5', '/shop/withdraw/requests?status=PAID', { token: shopToken, }); const settled = withdrawList.items?.find((w) => w.id === applied.id); if (!settled || settled.status !== 'PAID') { throw new Error('Expected settled withdraw request on shop side'); } const overdue = await req('HQ_WEB', '/admin/store-withdrawals/overdue-summary', { token: admin.accessToken, }); if (typeof overdue.pendingCount !== 'number') { throw new Error('overdue-summary missing pendingCount'); } console.log('10. Refund ticket flow'); const order2 = await req('USER_H5', '/trade/orders', { method: 'POST', token: userToken, body: JSON.stringify({ productId: products[0].id, quantity: 2, addressId: addr.id }), }); await req('USER_H5', `/trade/orders/${order2.id}/pay`, { method: 'POST', token: userToken }); await req('USER_H5', `/trade/orders/${order2.id}/refund-requests`, { method: 'POST', token: userToken, body: JSON.stringify({ remark: 'smoke refund' }), }); const tickets = await req('HQ_WEB', '/admin/tickets?ticketType=REFUND', { token: admin.accessToken, }); const refundTicket = tickets.items?.find((t) => t.refId === order2.id); if (!refundTicket) throw new Error('Refund ticket not created'); await req('HQ_WEB', `/admin/tickets/${refundTicket.id}/approve`, { method: 'POST', token: admin.accessToken, body: JSON.stringify({}), }); const refundedOrder = await req('USER_H5', `/trade/orders/${order2.id}`, { token: userToken }); if (refundedOrder.status !== 'REFUNDED' || refundedOrder.payStatus !== 'REFUNDED') { throw new Error( `Refund order status expected REFUNDED/REFUNDED, got ${refundedOrder.status}/${refundedOrder.payStatus}`, ); } console.log('11. Partner phone gate'); const unknownPartner = await expectFail('PARTNER_H5', '/partner/auth/phone/check', { method: 'POST', body: JSON.stringify({ phone: '13899999999' }), }); if (!unknownPartner.includes('未找到合伙人账号')) { throw new Error(`Expected partner phone gate, got: ${unknownPartner}`); } console.log('12. Partner bill'); const partners = await req('HQ_WEB', '/admin/partners', { token: admin.accessToken }); const partnerId = partners.items?.[0]?.id; if (partnerId) { const now = new Date(); await req('HQ_WEB', '/admin/partner-bills/generate', { method: 'POST', token: admin.accessToken, body: JSON.stringify({ partnerId, year: now.getFullYear(), month: now.getMonth() + 1 }), }); } console.log('\n✅ V3 smoke passed'); } main().catch((e) => { console.error('❌ V3 smoke failed:', e.message); process.exit(1); });