@vobs/payment 0.3.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +182 -0
  3. package/dist/alipay/client.cjs +112 -0
  4. package/dist/alipay/client.cjs.map +1 -0
  5. package/dist/alipay/client.d.cts +36 -0
  6. package/dist/alipay/client.d.ts +36 -0
  7. package/dist/alipay/client.js +110 -0
  8. package/dist/alipay/client.js.map +1 -0
  9. package/dist/alipay/config.cjs +67 -0
  10. package/dist/alipay/config.cjs.map +1 -0
  11. package/dist/alipay/config.d.cts +30 -0
  12. package/dist/alipay/config.d.ts +30 -0
  13. package/dist/alipay/config.js +65 -0
  14. package/dist/alipay/config.js.map +1 -0
  15. package/dist/alipay/index.cjs +592 -0
  16. package/dist/alipay/index.cjs.map +1 -0
  17. package/dist/alipay/index.d.cts +12 -0
  18. package/dist/alipay/index.d.ts +12 -0
  19. package/dist/alipay/index.js +580 -0
  20. package/dist/alipay/index.js.map +1 -0
  21. package/dist/alipay/notify.cjs +70 -0
  22. package/dist/alipay/notify.cjs.map +1 -0
  23. package/dist/alipay/notify.d.cts +52 -0
  24. package/dist/alipay/notify.d.ts +52 -0
  25. package/dist/alipay/notify.js +68 -0
  26. package/dist/alipay/notify.js.map +1 -0
  27. package/dist/alipay/payment.cjs +49 -0
  28. package/dist/alipay/payment.cjs.map +1 -0
  29. package/dist/alipay/payment.d.cts +24 -0
  30. package/dist/alipay/payment.d.ts +24 -0
  31. package/dist/alipay/payment.js +47 -0
  32. package/dist/alipay/payment.js.map +1 -0
  33. package/dist/alipay/plugin.cjs +300 -0
  34. package/dist/alipay/plugin.cjs.map +1 -0
  35. package/dist/alipay/plugin.d.cts +43 -0
  36. package/dist/alipay/plugin.d.ts +43 -0
  37. package/dist/alipay/plugin.js +296 -0
  38. package/dist/alipay/plugin.js.map +1 -0
  39. package/dist/alipay/query.cjs +80 -0
  40. package/dist/alipay/query.cjs.map +1 -0
  41. package/dist/alipay/query.d.cts +26 -0
  42. package/dist/alipay/query.d.ts +26 -0
  43. package/dist/alipay/query.js +78 -0
  44. package/dist/alipay/query.js.map +1 -0
  45. package/dist/alipay/refund.cjs +104 -0
  46. package/dist/alipay/refund.cjs.map +1 -0
  47. package/dist/alipay/refund.d.cts +32 -0
  48. package/dist/alipay/refund.d.ts +32 -0
  49. package/dist/alipay/refund.js +102 -0
  50. package/dist/alipay/refund.js.map +1 -0
  51. package/dist/alipay/response.cjs +38 -0
  52. package/dist/alipay/response.cjs.map +1 -0
  53. package/dist/alipay/response.d.cts +19 -0
  54. package/dist/alipay/response.d.ts +19 -0
  55. package/dist/alipay/response.js +35 -0
  56. package/dist/alipay/response.js.map +1 -0
  57. package/dist/alipay/types.cjs +4 -0
  58. package/dist/alipay/types.cjs.map +1 -0
  59. package/dist/alipay/types.d.cts +242 -0
  60. package/dist/alipay/types.d.ts +242 -0
  61. package/dist/alipay/types.js +3 -0
  62. package/dist/alipay/types.js.map +1 -0
  63. package/dist/alipay/wap.cjs +50 -0
  64. package/dist/alipay/wap.cjs.map +1 -0
  65. package/dist/alipay/wap.d.cts +24 -0
  66. package/dist/alipay/wap.d.ts +24 -0
  67. package/dist/alipay/wap.js +48 -0
  68. package/dist/alipay/wap.js.map +1 -0
  69. package/dist/index-B1aUR7Vt.d.ts +49 -0
  70. package/dist/index-B8pzZxGq.d.cts +44 -0
  71. package/dist/index-BWnIrwRi.d.ts +44 -0
  72. package/dist/index-Dyrkl86r.d.cts +49 -0
  73. package/dist/index.cjs +1071 -0
  74. package/dist/index.cjs.map +1 -0
  75. package/dist/index.d.cts +23 -0
  76. package/dist/index.d.ts +23 -37
  77. package/dist/index.js +1052 -37
  78. package/dist/index.js.map +1 -0
  79. package/dist/wechat/client.cjs +119 -0
  80. package/dist/wechat/client.cjs.map +1 -0
  81. package/dist/wechat/client.d.cts +39 -0
  82. package/dist/wechat/client.d.ts +39 -0
  83. package/dist/wechat/client.js +117 -0
  84. package/dist/wechat/client.js.map +1 -0
  85. package/dist/wechat/config.cjs +76 -0
  86. package/dist/wechat/config.cjs.map +1 -0
  87. package/dist/wechat/config.d.cts +29 -0
  88. package/dist/wechat/config.d.ts +29 -0
  89. package/dist/wechat/config.js +74 -0
  90. package/dist/wechat/config.js.map +1 -0
  91. package/dist/wechat/index.cjs +612 -0
  92. package/dist/wechat/index.cjs.map +1 -0
  93. package/dist/wechat/index.d.cts +10 -0
  94. package/dist/wechat/index.d.ts +10 -0
  95. package/dist/wechat/index.js +602 -0
  96. package/dist/wechat/index.js.map +1 -0
  97. package/dist/wechat/notify.cjs +73 -0
  98. package/dist/wechat/notify.cjs.map +1 -0
  99. package/dist/wechat/notify.d.cts +53 -0
  100. package/dist/wechat/notify.d.ts +53 -0
  101. package/dist/wechat/notify.js +71 -0
  102. package/dist/wechat/notify.js.map +1 -0
  103. package/dist/wechat/payment.cjs +186 -0
  104. package/dist/wechat/payment.cjs.map +1 -0
  105. package/dist/wechat/payment.d.cts +50 -0
  106. package/dist/wechat/payment.d.ts +50 -0
  107. package/dist/wechat/payment.js +184 -0
  108. package/dist/wechat/payment.js.map +1 -0
  109. package/dist/wechat/plugin.cjs +305 -0
  110. package/dist/wechat/plugin.cjs.map +1 -0
  111. package/dist/wechat/plugin.d.cts +40 -0
  112. package/dist/wechat/plugin.d.ts +40 -0
  113. package/dist/wechat/plugin.js +301 -0
  114. package/dist/wechat/plugin.js.map +1 -0
  115. package/dist/wechat/refund.cjs +112 -0
  116. package/dist/wechat/refund.cjs.map +1 -0
  117. package/dist/wechat/refund.d.cts +26 -0
  118. package/dist/wechat/refund.d.ts +26 -0
  119. package/dist/wechat/refund.js +110 -0
  120. package/dist/wechat/refund.js.map +1 -0
  121. package/dist/wechat/response.cjs +50 -0
  122. package/dist/wechat/response.cjs.map +1 -0
  123. package/dist/wechat/response.d.cts +20 -0
  124. package/dist/wechat/response.d.ts +20 -0
  125. package/dist/wechat/response.js +46 -0
  126. package/dist/wechat/response.js.map +1 -0
  127. package/dist/wechat/types.cjs +4 -0
  128. package/dist/wechat/types.cjs.map +1 -0
  129. package/dist/wechat/types.d.cts +200 -0
  130. package/dist/wechat/types.d.ts +200 -0
  131. package/dist/wechat/types.js +3 -0
  132. package/dist/wechat/types.js.map +1 -0
  133. package/package.json +40 -27
  134. package/src/alipay/alipay.test.ts +231 -0
  135. package/src/alipay/client.ts +67 -0
  136. package/src/alipay/config.ts +72 -0
  137. package/src/alipay/index.ts +32 -0
  138. package/src/alipay/notify.ts +92 -0
  139. package/src/alipay/payment.ts +60 -0
  140. package/src/alipay/plugin.ts +64 -0
  141. package/src/alipay/query.ts +52 -0
  142. package/src/alipay/refund.ts +85 -0
  143. package/src/alipay/response.ts +40 -0
  144. package/src/alipay/types.ts +271 -0
  145. package/src/alipay/wap.ts +55 -0
  146. package/src/index.ts +78 -0
  147. package/src/wechat/client.ts +68 -0
  148. package/src/wechat/config.ts +81 -0
  149. package/src/wechat/index.ts +30 -0
  150. package/src/wechat/notify.ts +93 -0
  151. package/src/wechat/payment.ts +177 -0
  152. package/src/wechat/plugin.ts +59 -0
  153. package/src/wechat/refund.ts +80 -0
  154. package/src/wechat/response.ts +54 -0
  155. package/src/wechat/types.ts +223 -0
  156. package/src/wechat/wechat.test.ts +393 -0
  157. package/dist/alipay-official.d.ts +0 -34
  158. package/dist/alipay-official.js +0 -206
  159. package/dist/handlers.d.ts +0 -45
  160. package/dist/handlers.js +0 -105
  161. package/dist/jeepay.d.ts +0 -39
  162. package/dist/jeepay.js +0 -206
  163. package/dist/lakala.d.ts +0 -41
  164. package/dist/lakala.js +0 -243
  165. package/dist/manager.d.ts +0 -67
  166. package/dist/manager.js +0 -538
  167. package/dist/mock-driver.d.ts +0 -22
  168. package/dist/mock-driver.js +0 -59
  169. package/dist/state-machine.d.ts +0 -41
  170. package/dist/state-machine.js +0 -60
  171. package/dist/store.d.ts +0 -33
  172. package/dist/store.js +0 -76
  173. package/dist/types.d.ts +0 -168
  174. package/dist/types.js +0 -11
  175. package/dist/verify.d.ts +0 -34
  176. package/dist/verify.js +0 -55
  177. package/dist/wechat-official.d.ts +0 -40
  178. package/dist/wechat-official.js +0 -208
package/dist/handlers.js DELETED
@@ -1,105 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/payment
4
- */
5
- /**
6
- * Framework-agnostic HTTP handler layer — `paymentHttpDispatch` returns standard responses that
7
- * the business wraps in one line inside its own server (Hono/Express/Fastify). This package does
8
- * not bind to any server.
9
- *
10
- * Route conventions (per design doc §3.3/§3.5):
11
- * GET /payment/available-methods available payment methods
12
- * POST /api/payment/orders create order (server-side pricing + limit + idempotency + duplicate guard)
13
- * GET /api/payment/orders/:id/status query status (frontend polling)
14
- * POST /api/payment/orders/:id/close close on timeout/cancel (example helper)
15
- * POST /api/payment/orders/:id/confirm manual release for large-amount review (admin; business-side auth required)
16
- * POST /payment/callback/:driver channel callback (verify + anti-replay + state machine)
17
- */
18
- const json = (status, body) => ({ status, body });
19
- function errorStatus(err) {
20
- switch (err.error) {
21
- case 'order_not_found':
22
- return 404;
23
- case 'amount_exceeds_limit':
24
- case 'amount_mismatch':
25
- case 'bad_request':
26
- return 400;
27
- case 'order_conflict':
28
- return 409;
29
- case 'channel_unavailable':
30
- return 503;
31
- default:
32
- return 500;
33
- }
34
- }
35
- /** Create a dispatch function (pure logic, no framework dependency). */
36
- export function paymentHttpDispatch(manager, options = {}) {
37
- const authorize = async (route, orderId, request) => (await options.authorize?.({ route, orderId, request })) === true;
38
- return async function dispatch(ctx) {
39
- const { method, path } = ctx;
40
- if (method === 'GET' && path === '/payment/available-methods') {
41
- return json(200, { methods: manager.available() });
42
- }
43
- if (method === 'POST' && path === '/api/payment/orders') {
44
- let req;
45
- try {
46
- req = JSON.parse(ctx.body ?? '');
47
- }
48
- catch {
49
- return json(400, { error: 'bad_json' });
50
- }
51
- if (typeof req !== 'object' || req === null || Array.isArray(req)) {
52
- return json(400, { error: 'bad_request' });
53
- }
54
- const outcome = await manager.createOrder(req);
55
- if ('error' in outcome) {
56
- return json(errorStatus(outcome), outcome);
57
- }
58
- return json(200, outcome);
59
- }
60
- const statusMatch = path.match(/^\/api\/payment\/orders\/([^/]+)\/status$/);
61
- if (statusMatch && method === 'GET') {
62
- const orderId = statusMatch[1];
63
- if (!(await authorize('status', orderId, ctx)))
64
- return json(403, { error: 'forbidden' });
65
- const status = manager.getStatus(orderId);
66
- if (!status)
67
- return json(404, { error: 'order_not_found' });
68
- return json(200, { orderId, ...status });
69
- }
70
- const closeMatch = path.match(/^\/api\/payment\/orders\/([^/]+)\/close$/);
71
- if (closeMatch && method === 'POST') {
72
- const orderId = closeMatch[1];
73
- if (!(await authorize('close', orderId, ctx)))
74
- return json(403, { error: 'forbidden' });
75
- const status = manager.close(orderId);
76
- if (!status)
77
- return json(404, { error: 'order_not_found' });
78
- return json(200, { orderId, ...status });
79
- }
80
- const confirmMatch = path.match(/^\/api\/payment\/orders\/([^/]+)\/confirm$/);
81
- if (confirmMatch && method === 'POST') {
82
- const orderId = confirmMatch[1];
83
- if (!(await authorize('confirm', orderId, ctx)))
84
- return json(403, { error: 'forbidden' });
85
- const status = manager.confirm(orderId);
86
- if (!status)
87
- return json(404, { error: 'order_not_found' });
88
- return json(200, { orderId, ...status });
89
- }
90
- const callbackMatch = path.match(/^\/payment\/callback\/([^/]+)$/);
91
- if (callbackMatch && method === 'POST') {
92
- const driver = callbackMatch[1];
93
- const result = await manager.handleCallback(driver, {
94
- headers: ctx.headers ?? {},
95
- body: ctx.body ?? '',
96
- });
97
- // Channel-agreed response protocol: if text exists → plain text (WeChat/Jeepay SUCCESS, Alipay success), else JSON
98
- if (result.text !== undefined) {
99
- return { status: result.status, body: result.text };
100
- }
101
- return json(result.status, result.body);
102
- }
103
- return json(404, { error: 'not_found' });
104
- };
105
- }
package/dist/jeepay.d.ts DELETED
@@ -1,39 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/payment
4
- */
5
- /**
6
- * Jeepay (Jiquan Payment) aggregator driver — open-source aggregator payment system (LGPL-3.0).
7
- *
8
- * Integration shape: merchant business system → Jeepay payment gateway (self-hosted or official cloud pay.jeepay.vip).
9
- * Docs: docs.jeequan.com/docs/jeepay/payment_api (refer to the latest official docs for field details).
10
- *
11
- * Scene mapping (wayCode → OrderInit):
12
- * - `pc` (no openid) → `WX_NATIVE` / `ALI_QR` → payDataType=codeUrl → `{kind:'qr'}`
13
- * - `h5` + openid (official account) → `WX_JSAPI` → payDataType=wxpay → `{kind:'jsapi'}`
14
- * - `h5` (no openid) → `ALI_WAP` / `WX_H5` → payDataType=payUrl → `{kind:'redirect'}`
15
- * - `miniapp` (webview) → `WX_LITE` → payDataType=wxpay → `{kind:'jsapi'}`
16
- * - extras.wayCode can be set explicitly (the business decides WeChat/Alipay channel)
17
- *
18
- * Security:
19
- * - Request/callback signing: MD5 (non-empty params in ASCII lexicographic order + &key=appSecret → uppercase MD5)
20
- * - Callbacks use the channel self-verification path; Jeepay notifications have no unique ID,
21
- * so anti-replay relies on signature verification + HTTPS + state-machine idempotency
22
- * (duplicate notifications for an already-paid order return success without re-booking)
23
- *
24
- * Dependencies: node:crypto (MD5). Zero third-party.
25
- */
26
- import type { PaymentDriver } from './types.js';
27
- import type { PaymentStore } from './store.js';
28
- export interface JeepayOptions {
29
- /** Merchant number (issued by the Jeepay ops platform). */
30
- readonly mchNo: string;
31
- /** Application ID (merchant app). */
32
- readonly appId: string;
33
- /** Signing secret (configured on the merchant app, used for MD5). */
34
- readonly appSecret: string;
35
- /** Payment gateway URL (self-hosted, e.g. https://pay.example.com; or official cloud https://pay.jeepay.vip). */
36
- readonly baseUrl: string;
37
- readonly fetchImpl?: typeof fetch;
38
- }
39
- export declare function createJeepayDriver(_store: PaymentStore, options: JeepayOptions): PaymentDriver;
package/dist/jeepay.js DELETED
@@ -1,206 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/payment
4
- */
5
- /**
6
- * Jeepay (Jiquan Payment) aggregator driver — open-source aggregator payment system (LGPL-3.0).
7
- *
8
- * Integration shape: merchant business system → Jeepay payment gateway (self-hosted or official cloud pay.jeepay.vip).
9
- * Docs: docs.jeequan.com/docs/jeepay/payment_api (refer to the latest official docs for field details).
10
- *
11
- * Scene mapping (wayCode → OrderInit):
12
- * - `pc` (no openid) → `WX_NATIVE` / `ALI_QR` → payDataType=codeUrl → `{kind:'qr'}`
13
- * - `h5` + openid (official account) → `WX_JSAPI` → payDataType=wxpay → `{kind:'jsapi'}`
14
- * - `h5` (no openid) → `ALI_WAP` / `WX_H5` → payDataType=payUrl → `{kind:'redirect'}`
15
- * - `miniapp` (webview) → `WX_LITE` → payDataType=wxpay → `{kind:'jsapi'}`
16
- * - extras.wayCode can be set explicitly (the business decides WeChat/Alipay channel)
17
- *
18
- * Security:
19
- * - Request/callback signing: MD5 (non-empty params in ASCII lexicographic order + &key=appSecret → uppercase MD5)
20
- * - Callbacks use the channel self-verification path; Jeepay notifications have no unique ID,
21
- * so anti-replay relies on signature verification + HTTPS + state-machine idempotency
22
- * (duplicate notifications for an already-paid order return success without re-booking)
23
- *
24
- * Dependencies: node:crypto (MD5). Zero third-party.
25
- */
26
- import { createHash } from 'node:crypto';
27
- /** Primitive value → string (params only allow primitives; objects/functions excluded). */
28
- function primitiveString(v) {
29
- return String(v);
30
- }
31
- /** Parse form-encoded / object payloads → string param table. */
32
- function toParams(payload) {
33
- const out = {};
34
- if (typeof payload === 'string') {
35
- for (const pair of payload.split('&')) {
36
- const eq = pair.indexOf('=');
37
- if (eq > 0) {
38
- const k = decodeURIComponent(pair.slice(0, eq));
39
- const v = decodeURIComponent(pair.slice(eq + 1));
40
- out[k] = v;
41
- }
42
- }
43
- }
44
- else if (payload && typeof payload === 'object') {
45
- for (const [k, v] of Object.entries(payload)) {
46
- if (v !== undefined && v !== null) {
47
- if (typeof v === 'object' || typeof v === 'function') {
48
- out[k] = JSON.stringify(v) ?? '';
49
- }
50
- else {
51
- // primitive branch: safe stringify (TS typeof narrowing on unknown leaves {}; the assert excludes it)
52
- out[k] = primitiveString(v);
53
- }
54
- }
55
- }
56
- }
57
- return out;
58
- }
59
- export function createJeepayDriver(_store, options) {
60
- const fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
61
- /** MD5 sign: non-empty params (excluding sign) ASCII-sorted + &key=appSecret → MD5 → uppercase. */
62
- function md5Sign(params) {
63
- const str = Object.keys(params)
64
- .filter((k) => k !== 'sign' && params[k] !== undefined && params[k] !== null && params[k] !== '')
65
- .sort()
66
- .map((k) => `${k}=${params[k]}`)
67
- .join('&');
68
- return createHash('md5').update(`${str}&key=${options.appSecret}`).digest('hex').toUpperCase();
69
- }
70
- function md5Verify(params) {
71
- const sign = params['sign'];
72
- if (!sign)
73
- return false;
74
- return md5Sign(params) === sign.toUpperCase();
75
- }
76
- /** Scene → wayCode (extras.wayCode wins when set). */
77
- function resolveWayCode(input) {
78
- const explicit = input.extras?.['wayCode'];
79
- if (typeof explicit === 'string' && explicit)
80
- return explicit;
81
- const openid = input.extras?.['openid'];
82
- if (input.scene === 'pc')
83
- return 'WX_NATIVE';
84
- if (input.scene === 'miniapp')
85
- return 'WX_LITE';
86
- if (typeof openid === 'string')
87
- return 'WX_JSAPI';
88
- return 'ALI_WAP';
89
- }
90
- /** Channel extra params (openid, etc.) → JSON string. */
91
- function channelExtraOf(input) {
92
- const extras = (input.extras ?? {});
93
- const openid = extras['openid'];
94
- if (typeof openid === 'string') {
95
- return JSON.stringify({ openid });
96
- }
97
- return undefined;
98
- }
99
- async function createOrder(input) {
100
- const params = {
101
- mchNo: options.mchNo,
102
- appId: options.appId,
103
- mchOrderNo: input.orderId,
104
- wayCode: resolveWayCode(input),
105
- amount: String(input.amount),
106
- currency: 'cny',
107
- subject: input.subject,
108
- body: input.subject,
109
- notifyUrl: input.notifyUrl,
110
- };
111
- const clientIp = input.extras?.['clientIp'];
112
- if (typeof clientIp === 'string' && clientIp)
113
- params['clientIp'] = clientIp;
114
- const channelExtra = channelExtraOf(input);
115
- if (channelExtra)
116
- params['channelExtra'] = channelExtra;
117
- params['sign'] = md5Sign(params);
118
- const res = await fetchImpl(`${options.baseUrl}/api/pay/unifiedOrder`, {
119
- method: 'POST',
120
- headers: { 'Content-Type': 'application/json' },
121
- body: JSON.stringify(params),
122
- });
123
- const body = (await res.json());
124
- if (body.code !== 0 || !body.data) {
125
- throw new Error(`jeepay_error:${body.code ?? 'unknown'}:${body.msg ?? ''}`);
126
- }
127
- const data = body.data;
128
- const payData = data.payData ?? '';
129
- if (data.payDataType === 'codeUrl') {
130
- return {
131
- kind: 'qr',
132
- qrUrl: payData,
133
- expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
134
- };
135
- }
136
- if (data.payDataType === 'payUrl') {
137
- return { kind: 'redirect', payUrl: payData };
138
- }
139
- if (data.payDataType === 'wxpay') {
140
- return { kind: 'jsapi', payParams: JSON.parse(payData) };
141
- }
142
- throw new Error(`unsupported_pay_data_type:${data.payDataType ?? 'unknown'}`);
143
- }
144
- function verifyCallback(payload, _headers) {
145
- const params = toParams(payload);
146
- if (!md5Verify(params)) {
147
- return { ok: false, reason: 'bad_signature' };
148
- }
149
- // state: 2 = success (0 unpaid / 1 paying / 2 success / 3 closed)
150
- const state = Number(params['state'] ?? params['orderState']);
151
- if (state !== 2) {
152
- return { ok: false, reason: `state:${state}` };
153
- }
154
- const orderId = params['mchOrderNo'];
155
- const amount = params['amount'];
156
- if (!orderId || amount === undefined) {
157
- return { ok: false, reason: 'bad_payload' };
158
- }
159
- return {
160
- ok: true,
161
- orderId,
162
- paidAmount: Number(amount),
163
- ...(params['payOrderId'] ? { channelTxnId: params['payOrderId'] } : {}),
164
- };
165
- }
166
- async function queryOrder(orderId) {
167
- try {
168
- const params = {
169
- mchNo: options.mchNo,
170
- appId: options.appId,
171
- mchOrderNo: orderId,
172
- };
173
- params['sign'] = md5Sign(params);
174
- const res = await fetchImpl(`${options.baseUrl}/api/pay/query`, {
175
- method: 'POST',
176
- headers: { 'Content-Type': 'application/json' },
177
- body: JSON.stringify(params),
178
- });
179
- const body = (await res.json());
180
- if (body.code !== 0 || !body.data) {
181
- return { state: 'unknown', raw: body };
182
- }
183
- // orderState: 0 unpaid / 1 paying / 2 success / 3 closed
184
- const state = body.data.orderState;
185
- if (state === 2) {
186
- return { state: 'paid' };
187
- }
188
- if (state === 3) {
189
- return { state: 'closed' };
190
- }
191
- return { state: 'created' };
192
- }
193
- catch {
194
- return { state: 'unknown', raw: { error: 'query_failed' } };
195
- }
196
- }
197
- return {
198
- name: 'jeepay',
199
- scenes: ['pc', 'h5', 'miniapp'],
200
- callbackVerification: 'driver',
201
- createOrder,
202
- verifyCallback,
203
- queryOrder,
204
- successResponseText: 'SUCCESS',
205
- };
206
- }
package/dist/lakala.d.ts DELETED
@@ -1,41 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/payment
4
- */
5
- /**
6
- * Lakala aggregator payment driver (licensed aggregator; main-scan / JSAPI / miniapp).
7
- *
8
- * Docs: o.lakala.com (aggregator main-scan /api/v3/labs/trans/preorder).
9
- *
10
- * Scene mapping (trans_type + account_type):
11
- * - `pc` → trans_type=41 NATIVE (main scan), account_type from extras.accountType
12
- * (WECHAT/ALIPAY/UQRCODEPAY, default WECHAT) → `{kind:'qr'}`
13
- * - `h5` + openid → trans_type=51 JSAPI (WeChat official account, acc_busi_fields.openid) → `{kind:'jsapi'}`
14
- * - `miniapp` → trans_type=71 WeChat miniapp (acc_busi_fields.openid) → `{kind:'jsapi'}`
15
- * - `h5` no openid → **unsupported** (main scan has no WAP form; aggregator cashier product API awaits
16
- * official confirmation, P2 extension)
17
- *
18
- * Security:
19
- * - Request/callback signing: merchant private key RSA-SHA256 (params minus sign, sorted, → base64); responses/callbacks verified with the Lakala public key
20
- * - Callbacks use the channel self-verification path; research confirms the callback expects a `success` text within 5s or Lakala retries
21
- *
22
- * ⚠️ Integration status: request fields come from the official docs (2026-03); **response structure / signature details /
23
- * callback field names follow the latest Lakala docs and must be calibrated during sandbox integration
24
- * (test.wsmsd.cn)** — this implementation follows industry convention with lenient parsing.
25
- */
26
- import type { PaymentDriver } from './types.js';
27
- import type { PaymentStore } from './store.js';
28
- export interface LakalaOptions {
29
- /** Merchant number assigned by Lakala. */
30
- readonly merchantNo: string;
31
- /** Business terminal number assigned by Lakala. */
32
- readonly termNo: string;
33
- /** Merchant private key (PEM, used for request signing). */
34
- readonly privateKeyPem: string;
35
- /** Lakala public key (PEM, used to verify callbacks). */
36
- readonly lakalaPublicKeyPem: string;
37
- /** Gateway URL. Production s2.lakala.com, test test.wsmsd.cn (path includes /api/v3). */
38
- readonly baseUrl?: string;
39
- readonly fetchImpl?: typeof fetch;
40
- }
41
- export declare function createLakalaDriver(_store: PaymentStore, options: LakalaOptions): PaymentDriver;
package/dist/lakala.js DELETED
@@ -1,243 +0,0 @@
1
- /** @license MIT
2
- * Copyright (c) 2026 vobsjs
3
- * @vobs/payment
4
- */
5
- /**
6
- * Lakala aggregator payment driver (licensed aggregator; main-scan / JSAPI / miniapp).
7
- *
8
- * Docs: o.lakala.com (aggregator main-scan /api/v3/labs/trans/preorder).
9
- *
10
- * Scene mapping (trans_type + account_type):
11
- * - `pc` → trans_type=41 NATIVE (main scan), account_type from extras.accountType
12
- * (WECHAT/ALIPAY/UQRCODEPAY, default WECHAT) → `{kind:'qr'}`
13
- * - `h5` + openid → trans_type=51 JSAPI (WeChat official account, acc_busi_fields.openid) → `{kind:'jsapi'}`
14
- * - `miniapp` → trans_type=71 WeChat miniapp (acc_busi_fields.openid) → `{kind:'jsapi'}`
15
- * - `h5` no openid → **unsupported** (main scan has no WAP form; aggregator cashier product API awaits
16
- * official confirmation, P2 extension)
17
- *
18
- * Security:
19
- * - Request/callback signing: merchant private key RSA-SHA256 (params minus sign, sorted, → base64); responses/callbacks verified with the Lakala public key
20
- * - Callbacks use the channel self-verification path; research confirms the callback expects a `success` text within 5s or Lakala retries
21
- *
22
- * ⚠️ Integration status: request fields come from the official docs (2026-03); **response structure / signature details /
23
- * callback field names follow the latest Lakala docs and must be calibrated during sandbox integration
24
- * (test.wsmsd.cn)** — this implementation follows industry convention with lenient parsing.
25
- */
26
- import { createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
27
- const DEFAULT_GATEWAY = 'https://s2.lakala.com/api/v3';
28
- /** Unknown value → string (objects JSON-serialized to avoid [object Object]). */
29
- function toStr(value) {
30
- return typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
31
- }
32
- /** Scene → trans_type (Lakala access-mode code). */
33
- function transTypeOf(scene) {
34
- if (scene === 'pc')
35
- return '41';
36
- if (scene === 'h5')
37
- return '51';
38
- if (scene === 'miniapp')
39
- return '71';
40
- return undefined;
41
- }
42
- export function createLakalaDriver(_store, options) {
43
- const baseUrl = options.baseUrl ?? DEFAULT_GATEWAY;
44
- const fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
45
- const merchantKey = createPrivateKey(options.privateKeyPem);
46
- const lakalaPublicKey = createPublicKey(options.lakalaPublicKeyPem);
47
- /** RSA sign: params (excluding sign/empty) sorted → SHA256withRSA → base64. */
48
- function rsaSign(params) {
49
- const str = Object.keys(params)
50
- .filter((k) => k !== 'sign' && params[k] !== undefined && params[k] !== null && params[k] !== '')
51
- .sort()
52
- .map((k) => `${k}=${params[k]}`)
53
- .join('&');
54
- return sign('RSA-SHA256', Buffer.from(str), merchantKey).toString('base64');
55
- }
56
- /** RSA verify (Lakala public key). */
57
- function rsaVerify(params) {
58
- const signature = params['sign'];
59
- if (!signature)
60
- return false;
61
- const str = Object.keys(params)
62
- .filter((k) => k !== 'sign' && params[k] !== undefined && params[k] !== null && params[k] !== '')
63
- .sort()
64
- .map((k) => `${k}=${params[k]}`)
65
- .join('&');
66
- try {
67
- return verify('RSA-SHA256', Buffer.from(str), lakalaPublicKey, Buffer.from(signature, 'base64'));
68
- }
69
- catch {
70
- return false;
71
- }
72
- }
73
- /** Location info (required for risk control). */
74
- function locationInfoOf(input) {
75
- const extras = (input.extras ?? {});
76
- const clientIp = typeof extras['clientIp'] === 'string' && extras['clientIp']
77
- ? extras['clientIp']
78
- : '127.0.0.1';
79
- return { request_ip: clientIp };
80
- }
81
- /** Account-side business info (JSAPI/miniapp scenes pass openid/userId per channel). */
82
- function accBusiFieldsOf(input, accountType) {
83
- const extras = (input.extras ?? {});
84
- const openid = extras['openid'];
85
- if (typeof openid === 'string' && openid) {
86
- return accountType === 'WECHAT' ? { openid } : { user_id: openid };
87
- }
88
- const userId = extras['userId'];
89
- if (typeof userId === 'string' && userId) {
90
- return { user_id: userId };
91
- }
92
- return undefined;
93
- }
94
- /** Wallet type: extras.accountType explicit (WECHAT/ALIPAY/UQRCODEPAY), default WECHAT. */
95
- function accountTypeOf(input) {
96
- const explicit = input.extras?.['accountType'];
97
- return typeof explicit === 'string' && explicit ? explicit : 'WECHAT';
98
- }
99
- async function createOrder(input) {
100
- const transType = transTypeOf(input.scene);
101
- if (!transType) {
102
- throw new Error(`unsupported_scene:${input.scene}`);
103
- }
104
- // JSAPI (51) / miniapp (71) require openid/userId
105
- if (input.scene === 'h5' || input.scene === 'miniapp') {
106
- const extras = (input.extras ?? {});
107
- if (typeof extras['openid'] !== 'string' && typeof extras['userId'] !== 'string') {
108
- throw new Error(`unsupported_scene:${input.scene} (JSAPI/小程序需 openid)`);
109
- }
110
- }
111
- const accountType = accountTypeOf(input);
112
- const params = {
113
- merchant_no: options.merchantNo,
114
- term_no: options.termNo,
115
- out_trade_no: input.orderId,
116
- account_type: accountType,
117
- trans_type: transType,
118
- total_amount: String(input.amount),
119
- busi_mode: 'ACQ',
120
- subject: input.subject,
121
- notify_url: input.notifyUrl,
122
- location_info: JSON.stringify(locationInfoOf(input)),
123
- };
124
- const accFields = accBusiFieldsOf(input, accountType);
125
- if (accFields) {
126
- params['acc_busi_fields'] = JSON.stringify(accFields);
127
- }
128
- params['sign'] = rsaSign(params);
129
- const res = await fetchImpl(`${baseUrl}/labs/trans/preorder`, {
130
- method: 'POST',
131
- headers: { 'Content-Type': 'application/json' },
132
- body: JSON.stringify(params),
133
- });
134
- const body = (await res.json());
135
- if (!res.ok || (body['code'] !== undefined && body['code'] !== '0000' && body['code'] !== 0)) {
136
- throw new Error(`lakala_error:${toStr(body['code'] ?? res.status)}:${toStr(body['msg'] ?? body['message'] ?? '')}`);
137
- }
138
- // ⚠️ response field names follow the latest Lakala docs (sandbox-calibrated); parse leniently
139
- const data = (body['data'] ?? body);
140
- if (transType === '41') {
141
- // main-scan: returns QR code content
142
- const qrCode = data['code_url'] ??
143
- data['codeUrl'] ??
144
- data['qr_code'] ??
145
- data['prepayCode'] ??
146
- data['code'];
147
- if (typeof qrCode === 'string' && qrCode) {
148
- return {
149
- kind: 'qr',
150
- qrUrl: qrCode,
151
- expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
152
- };
153
- }
154
- }
155
- else {
156
- // JSAPI/miniapp: returns pay params (JSON string)
157
- const payParam = data['pay_param'] ?? data['payParams'] ?? data['pay_params'] ?? data['pay_param_json'];
158
- if (typeof payParam === 'string' && payParam) {
159
- return { kind: 'jsapi', payParams: JSON.parse(payParam) };
160
- }
161
- }
162
- throw new Error(`lakala_unsupported_response:${JSON.stringify(body)}`);
163
- }
164
- function verifyCallback(payload, _headers) {
165
- // ⚠️ callback field names follow the latest official docs (sandbox-calibrated). JSON notification + sign-field verification.
166
- let obj;
167
- try {
168
- obj =
169
- typeof payload === 'string'
170
- ? JSON.parse(payload)
171
- : payload;
172
- }
173
- catch {
174
- return { ok: false, reason: 'bad_payload' };
175
- }
176
- const params = Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, String(v)]));
177
- if (!rsaVerify(params)) {
178
- return { ok: false, reason: 'bad_signature' };
179
- }
180
- // status field candidates (pay_status / trade_status / result_code); success values 'SUCCESS'/'00'/'0000'/2
181
- const status = String(params['pay_status'] ??
182
- params['trade_status'] ??
183
- params['result_code'] ??
184
- params['order_status'] ??
185
- '');
186
- const success = status === 'SUCCESS' ||
187
- status === '00' ||
188
- status === '0000' ||
189
- status === '2' ||
190
- status === 'TRADE_SUCCESS';
191
- if (!success) {
192
- return { ok: false, reason: `status:${status}` };
193
- }
194
- const orderId = params['out_trade_no'] ?? params['outTradeNo'];
195
- const amount = params['total_amount'] ?? params['amount'];
196
- if (!orderId || amount === undefined) {
197
- return { ok: false, reason: 'bad_payload' };
198
- }
199
- return {
200
- ok: true,
201
- orderId,
202
- paidAmount: Number(amount),
203
- ...(params['pay_order_no'] ? { channelTxnId: params['pay_order_no'] } : {}),
204
- };
205
- }
206
- async function queryOrder(orderId) {
207
- try {
208
- const params = {
209
- merchant_no: options.merchantNo,
210
- term_no: options.termNo,
211
- out_trade_no: orderId,
212
- };
213
- params['sign'] = rsaSign(params);
214
- const res = await fetchImpl(`${baseUrl}/trade/query`, {
215
- method: 'POST',
216
- headers: { 'Content-Type': 'application/json' },
217
- body: JSON.stringify(params),
218
- });
219
- const body = (await res.json());
220
- const data = (body['data'] ?? body);
221
- const status = toStr(data['pay_status'] ?? data['trade_status'] ?? data['order_status'] ?? '');
222
- if (status === 'SUCCESS' || status === '00' || status === 'TRADE_SUCCESS') {
223
- return { state: 'paid' };
224
- }
225
- if (status === 'CLOSED' || status === 'CLOSE') {
226
- return { state: 'closed' };
227
- }
228
- return { state: 'created' };
229
- }
230
- catch {
231
- return { state: 'unknown', raw: { error: 'query_failed' } };
232
- }
233
- }
234
- return {
235
- name: 'lakala',
236
- scenes: ['pc', 'h5', 'miniapp'],
237
- callbackVerification: 'driver',
238
- createOrder,
239
- verifyCallback,
240
- queryOrder,
241
- successResponseText: 'success',
242
- };
243
- }