virtualPayFulfillment.test.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { fulfillOrder, MiaoguoVirtualPayNotify500 } from '../src/api/virtualPay/virtualPayController.js';
  4. function orderXml({ productId = '166_199', env = 0, goodsPrice = 19900, detail = {} } = {}) {
  5. return `<xml>`
  6. + `<detail><![CDATA[${JSON.stringify(detail)}]]></detail>`
  7. + `<virtual_pay><![CDATA[${JSON.stringify({ productId, env, goodsPrice })}]]></virtual_pay>`
  8. + `</xml>`;
  9. }
  10. function createFakeConnection({ order, user }) {
  11. const state = {
  12. began: 0,
  13. committed: 0,
  14. rolledBack: 0,
  15. released: 0,
  16. queries: []
  17. };
  18. const connection = {
  19. async beginTransaction() { state.began += 1; },
  20. async commit() { state.committed += 1; },
  21. async rollback() { state.rolledBack += 1; },
  22. release() { state.released += 1; },
  23. async query(sql, params) {
  24. state.queries.push({ sql, params });
  25. if (sql.includes('FROM ProductPayInfo')) return [[order]];
  26. if (sql.includes('FROM MiaoguoWXUsers')) return [[user]];
  27. return [{ affectedRows: 1 }];
  28. }
  29. };
  30. return { connection, state };
  31. }
  32. function dependencies(connection, postCalls = []) {
  33. return {
  34. getConnection: async () => connection,
  35. schedulePostCommit: callback => callback(),
  36. runPostPayActions: async payload => { postCalls.push(payload); }
  37. };
  38. }
  39. function paidMemberOrder(overrides = {}) {
  40. return {
  41. ID: 101,
  42. TradeNo: 'VP_TEST_199',
  43. PayType: 7,
  44. OpenID: 'openid-test',
  45. Money: 19900,
  46. Status: 0,
  47. XMLPre: orderXml({ detail: { EndTime: '2027-08-27 23:59:59' } }),
  48. ...overrides
  49. };
  50. }
  51. const memberUser = {
  52. UserID: 1,
  53. OpenID: 'openid-test',
  54. IsMember: 1,
  55. ProductServiceTime: '2026-08-27 23:59:59'
  56. };
  57. test('支付成功后在一个事务中更新会员、优惠券和订单', async () => {
  58. const { connection, state } = createFakeConnection({
  59. order: paidMemberOrder(),
  60. user: memberUser
  61. });
  62. const postCalls = [];
  63. const result = await fulfillOrder('VP_TEST_199', {
  64. openid: 'openid-test',
  65. env: 0,
  66. productId: '166_199',
  67. quantity: 1,
  68. originalPrice: 19900,
  69. actualPrice: 19900,
  70. paidTime: 1787846400,
  71. raw: { source: 'test' }
  72. }, dependencies(connection, postCalls));
  73. await new Promise(resolve => setImmediate(resolve));
  74. assert.equal(result.alreadyFulfilled, false);
  75. assert.equal(state.began, 1);
  76. assert.equal(state.committed, 1);
  77. assert.equal(state.rolledBack, 0);
  78. assert.equal(state.released, 1);
  79. assert.ok(state.queries.some(item => item.sql.includes('UPDATE MiaoguoWXUsers SET')));
  80. assert.ok(state.queries.some(item => item.sql.includes('UPDATE MiaoguoCoupon SET IsUse=1')));
  81. assert.ok(state.queries.some(item => item.sql.includes('SET PayEndTime=') && item.sql.includes('Status=1')));
  82. assert.equal(postCalls.length, 1);
  83. });
  84. test('测试用户的 1 元购买执行与正式购买相同的会员完成逻辑', async () => {
  85. const testOrder = paidMemberOrder({
  86. TradeNo: 'VP_TEST_BUY_1',
  87. Money: 100,
  88. XMLPre: orderXml({
  89. productId: '166_1',
  90. goodsPrice: 100,
  91. detail: { EndTime: '2027-08-27 23:59:59' }
  92. })
  93. });
  94. const { connection, state } = createFakeConnection({ order: testOrder, user: memberUser });
  95. const result = await fulfillOrder('VP_TEST_BUY_1', {
  96. openid: 'openid-test',
  97. env: 0,
  98. productId: '166_1',
  99. originalPrice: 100,
  100. actualPrice: 100
  101. }, dependencies(connection));
  102. assert.equal(result.memberUpdate.paidMember, true);
  103. assert.ok(state.queries.some(item => item.sql.includes('UPDATE MiaoguoCoupon SET IsUse=1')));
  104. assert.ok(state.queries.some(item => item.sql.includes('Status=1')));
  105. });
  106. test('重复发货通知不会重复增加会员有效期', async () => {
  107. const { connection, state } = createFakeConnection({
  108. order: paidMemberOrder({ Status: 1 }),
  109. user: memberUser
  110. });
  111. const result = await fulfillOrder('VP_TEST_199', {}, dependencies(connection));
  112. assert.equal(result.alreadyFulfilled, true);
  113. assert.equal(state.committed, 1);
  114. assert.equal(state.rolledBack, 0);
  115. assert.equal(state.queries.filter(item => item.sql.includes('UPDATE ')).length, 0);
  116. });
  117. test('微信实付金额不一致时回滚且不发放权益', async () => {
  118. const { connection, state } = createFakeConnection({
  119. order: paidMemberOrder(),
  120. user: memberUser
  121. });
  122. await assert.rejects(
  123. fulfillOrder('VP_TEST_199', {
  124. openid: 'openid-test',
  125. env: 0,
  126. productId: '166_199',
  127. orderFee: 100
  128. }, dependencies(connection)),
  129. /金额与本地订单不一致/
  130. );
  131. assert.equal(state.committed, 0);
  132. assert.equal(state.rolledBack, 1);
  133. assert.equal(state.released, 1);
  134. assert.equal(state.queries.filter(item => item.sql.includes('UPDATE ')).length, 0);
  135. });
  136. test('1 元试用只记录申请和支付订单,不开通付费会员', async () => {
  137. const trialOrder = paidMemberOrder({
  138. TradeNo: 'VP_TEST_1',
  139. PayType: 9,
  140. Money: 100,
  141. XMLPre: orderXml({ productId: '166_1', goodsPrice: 100, detail: { PayType: 9 } })
  142. });
  143. const { connection, state } = createFakeConnection({ order: trialOrder, user: memberUser });
  144. const result = await fulfillOrder('VP_TEST_1', {
  145. openid: 'openid-test',
  146. env: 0,
  147. productId: '166_1',
  148. originalPrice: 100,
  149. actualPrice: 100
  150. }, dependencies(connection));
  151. assert.equal(result.memberUpdate.paidMember, false);
  152. assert.equal(result.memberUpdate.IsApply, 1);
  153. assert.ok(state.queries.some(item => item.sql.includes('UPDATE MiaoguoWXUsers SET')));
  154. assert.equal(state.queries.some(item => item.sql.includes('UPDATE MiaoguoCoupon')), false);
  155. assert.ok(state.queries.some(item => item.sql.includes('Status=1')));
  156. });
  157. test('可在本地模拟微信发货通知并进入统一完成函数', async () => {
  158. const calls = [];
  159. const ctx = {
  160. query: {},
  161. request: {
  162. headers: { 'content-type': 'application/json' },
  163. body: {
  164. Event: 'xpay_goods_deliver_notify',
  165. OpenId: 'openid-test',
  166. OutTradeNo: 'VP_NOTIFY_TEST',
  167. Env: 0,
  168. GoodsInfo: {
  169. ProductId: '166_199',
  170. Quantity: 1,
  171. OrigPrice: 19900,
  172. ActualPrice: 19900
  173. },
  174. WeChatPayInfo: {
  175. TransactionId: 'wx-test-transaction',
  176. PaidTime: 1787846400
  177. }
  178. }
  179. }
  180. };
  181. await MiaoguoVirtualPayNotify500(ctx, {
  182. isValidMessageSignature: () => true,
  183. fulfillOrder: async (tradeNo, payment) => { calls.push({ tradeNo, payment }); }
  184. });
  185. assert.deepEqual(ctx.body, { ErrCode: 0, ErrMsg: 'success' });
  186. assert.equal(calls.length, 1);
  187. assert.equal(calls[0].tradeNo, 'VP_NOTIFY_TEST');
  188. assert.equal(calls[0].payment.productId, '166_199');
  189. assert.equal(calls[0].payment.actualPrice, 19900);
  190. });