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; } const MOCK_SMS_CODE = process.env.MOCK_SMS_CODE ?? '123456'; 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') { await sendSms(clientApp, phone, scene, sendPath); return req(clientApp, loginPath, { method: 'POST', body: JSON.stringify({ phone, code: MOCK_SMS_CODE }), }); } 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('8. Shop confirm redeem'); const shopLogin = await loginSms( 'SHOP_H5', '13900000001', 'STORE_LOGIN', '/shop/auth/login/sms', '/shop/auth/sms/send', ); const preview = await req('SHOP_H5', '/shop/redeem/preview', { method: 'POST', token: shopLogin.accessToken, 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: shopLogin.accessToken, body: JSON.stringify({ token: directToken.token }), }); console.log('9. Admin login + store payout'); const admin = await adminLogin(); const payouts = await req('HQ_WEB', '/admin/store-payouts?status=PENDING', { token: admin.accessToken, }); if (!payouts.items?.length) throw new Error('Expected pending store payout'); const payoutId = payouts.items[0].id; await req('HQ_WEB', `/admin/store-payouts/${payoutId}/confirm`, { method: 'POST', token: admin.accessToken, body: JSON.stringify({ remark: 'smoke confirm' }), }); 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({}), }); console.log('11. 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); });