virtualPayFulfillment.test.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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('会员有效期兼容小程序提交的斜杠日期且不触发 Moment 回退解析', async () => {
  107. const slashDateOrder = paidMemberOrder({
  108. TradeNo: 'VP_TEST_SLASH_DATE',
  109. Money: 100,
  110. XMLPre: orderXml({
  111. productId: '166_1',
  112. goodsPrice: 100,
  113. detail: { EndTime: '2032/05/14' }
  114. })
  115. });
  116. const { connection } = createFakeConnection({ order: slashDateOrder, user: memberUser });
  117. const result = await fulfillOrder('VP_TEST_SLASH_DATE', {
  118. openid: 'openid-test',
  119. env: 0,
  120. productId: '166_1',
  121. originalPrice: 100,
  122. actualPrice: 100
  123. }, dependencies(connection));
  124. assert.equal(result.memberUpdate.ProductServiceTime, '2032-05-14 00:00:00');
  125. });
  126. test('重复发货通知不会重复增加会员有效期', async () => {
  127. const { connection, state } = createFakeConnection({
  128. order: paidMemberOrder({ Status: 1 }),
  129. user: memberUser
  130. });
  131. const result = await fulfillOrder('VP_TEST_199', {}, dependencies(connection));
  132. assert.equal(result.alreadyFulfilled, true);
  133. assert.equal(state.committed, 1);
  134. assert.equal(state.rolledBack, 0);
  135. assert.equal(state.queries.filter(item => item.sql.includes('UPDATE ')).length, 0);
  136. });
  137. test('微信实付金额不一致时回滚且不发放权益', async () => {
  138. const { connection, state } = createFakeConnection({
  139. order: paidMemberOrder(),
  140. user: memberUser
  141. });
  142. await assert.rejects(
  143. fulfillOrder('VP_TEST_199', {
  144. openid: 'openid-test',
  145. env: 0,
  146. productId: '166_199',
  147. orderFee: 100
  148. }, dependencies(connection)),
  149. /金额与本地订单不一致/
  150. );
  151. assert.equal(state.committed, 0);
  152. assert.equal(state.rolledBack, 1);
  153. assert.equal(state.released, 1);
  154. assert.equal(state.queries.filter(item => item.sql.includes('UPDATE ')).length, 0);
  155. });
  156. test('1 元试用只记录申请和支付订单,不开通付费会员', async () => {
  157. const trialOrder = paidMemberOrder({
  158. TradeNo: 'VP_TEST_1',
  159. PayType: 9,
  160. Money: 100,
  161. XMLPre: orderXml({ productId: '166_1', goodsPrice: 100, detail: { PayType: 9 } })
  162. });
  163. const { connection, state } = createFakeConnection({ order: trialOrder, user: memberUser });
  164. const result = await fulfillOrder('VP_TEST_1', {
  165. openid: 'openid-test',
  166. env: 0,
  167. productId: '166_1',
  168. originalPrice: 100,
  169. actualPrice: 100
  170. }, dependencies(connection));
  171. assert.equal(result.memberUpdate.paidMember, false);
  172. assert.equal(result.memberUpdate.IsApply, 1);
  173. assert.ok(state.queries.some(item => item.sql.includes('UPDATE MiaoguoWXUsers SET')));
  174. assert.equal(state.queries.some(item => item.sql.includes('UPDATE MiaoguoCoupon')), false);
  175. assert.ok(state.queries.some(item => item.sql.includes('Status=1')));
  176. });
  177. test('可在本地模拟微信发货通知并进入统一完成函数', async () => {
  178. const calls = [];
  179. const ctx = {
  180. query: {},
  181. request: {
  182. headers: { 'content-type': 'application/json' },
  183. body: {
  184. Event: 'xpay_goods_deliver_notify',
  185. OpenId: 'openid-test',
  186. OutTradeNo: 'VP_NOTIFY_TEST',
  187. Env: 0,
  188. GoodsInfo: {
  189. ProductId: '166_199',
  190. Quantity: 1,
  191. OrigPrice: 19900,
  192. ActualPrice: 19900
  193. },
  194. WeChatPayInfo: {
  195. TransactionId: 'wx-test-transaction',
  196. PaidTime: 1787846400
  197. }
  198. }
  199. }
  200. };
  201. await MiaoguoVirtualPayNotify500(ctx, {
  202. isValidMessageSignature: () => true,
  203. fulfillOrder: async (tradeNo, payment) => { calls.push({ tradeNo, payment }); }
  204. });
  205. assert.deepEqual(ctx.body, { ErrCode: 0, ErrMsg: 'success' });
  206. assert.equal(calls.length, 1);
  207. assert.equal(calls[0].tradeNo, 'VP_NOTIFY_TEST');
  208. assert.equal(calls[0].payment.productId, '166_199');
  209. assert.equal(calls[0].payment.actualPrice, 19900);
  210. });