virtualPayFulfillment.test.js 8.6 KB

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