@wdft/micropayments-sdk 0.0.4 → 0.0.6

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.
package/src/index.test.ts CHANGED
@@ -3,64 +3,64 @@ import { getViaMsg } from "./index.js";
3
3
  import { mockFetch } from "./test_utils/mock_fetcher.js";
4
4
 
5
5
  it("getViaMsg", async () => {
6
- const mock = mockFetch();
6
+ const mock = mockFetch();
7
7
 
8
- await new Promise((resolve, reject) => {
9
- mock.next(200, {
10
- click_url: "mock_click_url",
11
- incoming_message_device: "mock_num",
12
- expires_at: Date.now() + 30_000,
13
- commitment_id: "sample_commitment_id",
14
- });
8
+ await new Promise((resolve, reject) => {
9
+ mock.next(200, {
10
+ click_url: "mock_click_url",
11
+ incoming_message_device: "mock_num",
12
+ expires_at: Date.now() + 30_000,
13
+ commitment_id: "sample_commitment_id",
14
+ });
15
15
 
16
- mock.next(200, {
17
- message: "waiting for sms",
18
- status: "waiting",
19
- commitment_id: "sample_commitment_id",
20
- });
16
+ mock.next(200, {
17
+ message: "waiting for sms",
18
+ status: "waiting",
19
+ commitment_id: "sample_commitment_id",
20
+ });
21
21
 
22
- mock.next(200, {
23
- message: "waiting for sms",
24
- status: "waiting",
25
- commitment_id: "sample_commitment_id",
26
- });
22
+ mock.next(200, {
23
+ message: "waiting for sms",
24
+ status: "waiting",
25
+ commitment_id: "sample_commitment_id",
26
+ });
27
27
 
28
- mock.next(200, {
29
- message: "waiting for sms",
30
- status: "resolved",
31
- commitment_id: "sample_commitment_id",
32
- });
28
+ mock.next(200, {
29
+ message: "waiting for sms",
30
+ status: "resolved",
31
+ commitment_id: "sample_commitment_id",
32
+ });
33
33
 
34
- getViaMsg(
35
- {
36
- amount: 10,
37
- reference_id: "ref",
38
- currency: "PLN",
39
- tenant_id: "my-tenant",
40
- endpoint: "http://localhost:8787",
41
- fetcher: mock.fetcher.bind(mock),
42
- retry_ms: 100,
43
- },
44
- (error, payload) => {
45
- if (!payload || error) {
46
- console.log(error);
47
- reject(error);
48
- return;
49
- }
34
+ getViaMsg(
35
+ {
36
+ amount: 10,
37
+ reference_id: "ref",
38
+ currency: "PLN",
39
+ tenant_id: "testing_dev_tenant",
40
+ endpoint: "http://localhost:8787",
41
+ fetcher: mock.fetcher.bind(mock),
42
+ retry_ms: 100,
43
+ },
44
+ (error, payload) => {
45
+ if (!payload || error) {
46
+ console.log(error);
47
+ reject(error);
48
+ return;
49
+ }
50
50
 
51
- const { status } = payload;
51
+ const { status } = payload;
52
52
 
53
- switch (status) {
54
- case "waiting":
55
- case "created":
56
- case "duplicated":
57
- case "expired":
58
- console.log(payload);
59
- break;
60
- case "resolved":
61
- resolve(status);
62
- }
63
- },
64
- );
65
- });
53
+ switch (status) {
54
+ case "waiting":
55
+ case "created":
56
+ case "duplicated":
57
+ case "expired":
58
+ console.log(payload);
59
+ break;
60
+ case "resolved":
61
+ resolve(status);
62
+ }
63
+ },
64
+ );
65
+ });
66
66
  });
package/src/index.ts CHANGED
@@ -4,23 +4,23 @@ import type { Fetcher } from "./service.js";
4
4
  import { Client } from "./client.js";
5
5
 
6
6
  export interface IGetViaMsgInit {
7
- reference_id: string;
8
- amount: number;
9
- currency: "PLN" | "EUR" | "GBP" | "USD" | "CHF" | string;
10
- tenant_id: string;
11
- endpoint?: string;
12
- fetcher?: Fetcher;
13
- retry_ms?: number;
7
+ reference_id: string;
8
+ amount: number;
9
+ currency: "PLN" | "EUR" | "GBP" | "USD" | "CHF" | string;
10
+ tenant_id: string;
11
+ endpoint?: string;
12
+ fetcher?: Fetcher;
13
+ retry_ms?: number;
14
14
  }
15
15
 
16
16
  export function getViaMsg(init: IGetViaMsgInit, callback: ICallback) {
17
- const client = Client.new({
18
- currency: init.currency,
19
- endpoint: init.endpoint ?? "https://api.micropayments.wdft.ovh/",
20
- tenant_id: init.tenant_id,
21
- fetcher: init.fetcher ?? fetch,
22
- retry_ms: init.retry_ms ?? 1000,
23
- });
17
+ const client = Client.new({
18
+ currency: init.currency,
19
+ endpoint: init.endpoint ?? "https://api.micropayments.wdft.ovh/",
20
+ tenant_id: init.tenant_id,
21
+ fetcher: init.fetcher ?? fetch,
22
+ retry_ms: init.retry_ms ?? 1000,
23
+ });
24
24
 
25
- client.create(init.reference_id, init.amount, callback);
25
+ client.create(init.reference_id, init.amount, callback);
26
26
  }
@@ -3,16 +3,16 @@ import { expect } from "vitest";
3
3
  import { mockFetch } from "./test_utils/mock_fetcher.js";
4
4
 
5
5
  it("mocking works", async () => {
6
- const mock = mockFetch();
6
+ const mock = mockFetch();
7
7
 
8
- mock.next(200, {
9
- test: 123,
10
- });
8
+ mock.next(200, {
9
+ test: 123,
10
+ });
11
11
 
12
- const response = await mock.fetcher();
12
+ const response = await mock.fetcher();
13
13
 
14
- expect(response.status).toBe(200);
15
- expect(await response.json()).toMatchObject({
16
- test: 123,
17
- });
14
+ expect(response.status).toBe(200);
15
+ expect(await response.json()).toMatchObject({
16
+ test: 123,
17
+ });
18
18
  });
@@ -4,50 +4,53 @@ import { Service } from "./service.js";
4
4
  import { mockFetch } from "./test_utils/mock_fetcher.js";
5
5
 
6
6
  it("service.createCommitment - 200 - resolved good data", async () => {
7
- const mock = mockFetch();
8
-
9
- const my = new Service({
10
- endpoint: "http://localhost:8787",
11
- tenant_id: "testing-dev-tenant",
12
- fetcher: () => mock.fetcher(),
13
- });
14
-
15
- mock.next(200, {
16
- click_url: "mock_click_url",
17
- incoming_message_device: "mock_num",
18
- expires_at: Date.now() + 30_000,
19
- commitment_id: "sample_commitment_id",
20
- });
21
-
22
- const commitment = await my.createCommitment("sample-content-id", 10, "PLN");
23
-
24
- expect(commitment).toMatchObject({
25
- click_url: "mock_click_url",
26
- incoming_message_device: "mock_num",
27
- commitment_id: "sample_commitment_id",
28
- });
7
+ const mock = mockFetch();
8
+
9
+ const my = new Service({
10
+ endpoint: "http://localhost:8787",
11
+ tenant_id: "testing_dev_tenant",
12
+ fetcher: () => mock.fetcher(),
13
+ });
14
+
15
+ mock.next(200, {
16
+ click_url: "mock_click_url",
17
+ incoming_message_device: "mock_num",
18
+ expires_at: Date.now() + 30_000,
19
+ commitment_id: "sample_commitment_id",
20
+ });
21
+
22
+ const commitment = await my.createCommitment("sample-content-id", 10, "PLN");
23
+
24
+ expect(commitment).toMatchObject({
25
+ click_url: "mock_click_url",
26
+ incoming_message_device: "mock_num",
27
+ commitment_id: "sample_commitment_id",
28
+ });
29
29
  });
30
30
 
31
31
  it("service.checkCommitment - 200 - resolved good data", async () => {
32
- const mock = mockFetch();
33
-
34
- const my = new Service({
35
- endpoint: "http://localhost:8787",
36
- tenant_id: "testing-dev-tenant",
37
- fetcher: () => mock.fetcher(),
38
- });
39
-
40
- mock.next(200, {
41
- message: "waiting for sms",
42
- status: "waiting",
43
- commitment_id: "sample_commitment_id",
44
- });
45
-
46
- const status = await my.checkCommitment("sample_commitment_id");
47
-
48
- expect(status).toMatchObject({
49
- message: "waiting for sms",
50
- status: "waiting",
51
- commitment_id: "sample_commitment_id",
52
- });
32
+ const mock = mockFetch();
33
+
34
+ const my = new Service({
35
+ endpoint: "http://localhost:8787",
36
+ tenant_id: "testing_dev_tenant",
37
+ fetcher: () => mock.fetcher(),
38
+ });
39
+
40
+ mock.next(200, {
41
+ message: "waiting for sms",
42
+ status: "waiting",
43
+ commitment_id: "sample_commitment_id",
44
+ }, {
45
+ 'Retry-After': '5'
46
+ });
47
+
48
+ const status = await my.checkCommitment("sample_commitment_id");
49
+
50
+ expect(status).toMatchObject({
51
+ message: "waiting for sms",
52
+ status: "waiting",
53
+ retry_after: 5000,
54
+ commitment_id: "sample_commitment_id",
55
+ });
53
56
  });
package/src/service.ts CHANGED
@@ -1,177 +1,215 @@
1
1
  interface CustomResponse {
2
- status: number;
3
- error: boolean;
2
+ status: number;
3
+ error: boolean;
4
4
  }
5
5
 
6
6
  interface SuccessResponse<T> extends CustomResponse {
7
- error: false;
8
- message: null;
9
- data: T;
7
+ error: false;
8
+ retry_after: number;
9
+ message: null;
10
+ data: T;
10
11
  }
11
12
 
12
13
  interface ErrorResponse extends CustomResponse {
13
- error: true;
14
- message: string;
15
- data: null;
14
+ error: true;
15
+ message: string;
16
+ data: null;
16
17
  }
17
18
 
18
19
  interface ICommitment {
19
- click_url: string;
20
- incoming_message_device: string;
21
- expires_at: number;
22
- commitment_id: string;
20
+ click_url: string;
21
+ incoming_message_device: string;
22
+ expires_at: number;
23
+ commitment_id: string;
24
+ }
25
+
26
+ export interface ICommitmentMetadata {
27
+ title: string;
28
+ url: string;
23
29
  }
24
30
 
25
31
  type ResponseDTO<T> = SuccessResponse<T> | ErrorResponse;
26
32
 
27
33
  async function parseResponse<T = unknown>(response: Response): Promise<T> {
28
- if (response.headers.get("Content-Type")?.includes("application/json")) {
29
- return (await response.json()) as T;
30
- }
31
- return (await response.text()) as T;
34
+ if (response.headers.get("Content-Type")?.includes("application/json")) {
35
+ return (await response.json()) as T;
36
+ }
37
+ return (await response.text()) as T;
32
38
  }
33
39
 
34
40
  export type Fetcher = (
35
- input: RequestInfo | URL,
36
- init?: RequestInit,
41
+ input: RequestInfo | URL,
42
+ init?: RequestInit,
37
43
  ) => Promise<Response>;
38
44
 
45
+ function randomBetween(min: number, max: number) {
46
+ return Math.floor(Math.random() * (max - min + 1)) + min;
47
+ }
48
+
49
+ function parseRetryAfter(retryAfter: string | undefined | null | number): number {
50
+ let retryAfterParsed = 0;
51
+
52
+ if (!retryAfter) {
53
+ return retryAfterParsed;
54
+ }
55
+
56
+ const retryAfterSeconds = typeof retryAfter === 'number' ? retryAfter : parseInt(retryAfter, 10);
57
+
58
+ if (!isNaN(retryAfterSeconds) && isFinite(retryAfterSeconds)) {
59
+ retryAfterParsed = retryAfterSeconds * 1000;
60
+ } else {
61
+ retryAfterParsed = 0;
62
+ }
63
+
64
+ return retryAfterParsed;
65
+ }
66
+
39
67
  async function request<T = unknown>(
40
- path: string,
41
- req: RequestInit,
42
- fetcher: Fetcher,
68
+ path: string,
69
+ req: RequestInit,
70
+ fetcher: Fetcher,
43
71
  ): Promise<ResponseDTO<T | null>> {
44
- try {
45
- const response = await fetcher(new Request(path, req));
46
- if (response.ok) {
47
- return {
48
- status: response.status,
49
- error: false,
50
- data: (await parseResponse(response)) as T,
51
- } as ResponseDTO<T>;
52
- }
53
- const errorPayload = await parseResponse(response);
54
- return {
55
- status: response.status,
56
- error:
57
- errorPayload &&
58
- typeof errorPayload === "object" &&
59
- "error" in errorPayload
60
- ? errorPayload.error
61
- : true,
62
- message:
63
- errorPayload &&
64
- typeof errorPayload === "object" &&
65
- "message" in errorPayload
66
- ? errorPayload.message
67
- : response.statusText,
68
- data: null,
69
- } as ResponseDTO<T>;
70
- } catch (error) {
71
- return {
72
- status: 500,
73
- error: true,
74
- message: error instanceof Error ? error.message : `${error}`,
75
- data: null,
76
- } as ErrorResponse;
77
- }
72
+ try {
73
+ const response = await fetcher(new Request(path, req));
74
+ if (response.ok) {
75
+ const retryAfter = response.headers.get('Retry-After');
76
+ return {
77
+ status: response.status,
78
+ error: false,
79
+ retry_after: parseRetryAfter(response.headers.get('Retry-After')),
80
+ data: (await parseResponse(response)) as T,
81
+ } as ResponseDTO<T>;
82
+ }
83
+ const errorPayload = await parseResponse(response);
84
+ return {
85
+ status: response.status,
86
+ error: errorPayload &&
87
+ typeof errorPayload === "object" &&
88
+ "error" in errorPayload
89
+ ? errorPayload.error
90
+ : true,
91
+ message: errorPayload &&
92
+ typeof errorPayload === "object" &&
93
+ "message" in errorPayload
94
+ ? errorPayload.message
95
+ : response.statusText,
96
+ retry_after: parseRetryAfter(response.headers.get('Retry-After')),
97
+ data: null,
98
+ } as ResponseDTO<T>;
99
+ } catch (error) {
100
+ return {
101
+ status: 500,
102
+ error: true,
103
+ message: error instanceof Error ? error.message : `${error}`,
104
+ retry_after: randomBetween(10_000, 30_000),
105
+ data: null,
106
+ } as ErrorResponse;
107
+ }
78
108
  }
79
109
 
80
110
  function buildUrl(endpoint: string, path: string) {
81
- const validEndpoint = endpoint.endsWith("/")
82
- ? endpoint.substring(0, endpoint.length - 1)
83
- : endpoint;
84
- const validPath = path.startsWith("/") ? path : "/".concat(path);
85
- return validEndpoint + validPath;
111
+ const validEndpoint = endpoint.endsWith("/")
112
+ ? endpoint.substring(0, endpoint.length - 1)
113
+ : endpoint;
114
+ const validPath = path.startsWith("/") ? path : "/".concat(path);
115
+ return validEndpoint + validPath;
86
116
  }
87
117
 
88
118
  export interface IServiceOptions {
89
- tenant_id: string;
90
- endpoint: string;
91
- fetcher?: Fetcher;
119
+ tenant_id: string;
120
+ endpoint: string;
121
+ fetcher?: Fetcher;
92
122
  }
93
123
 
94
124
  export interface ICommitmentStatus {
95
- commitment_id: string;
96
- status: string;
97
- message: string;
125
+ commitment_id: string;
126
+ status: string;
127
+ message: string;
128
+ retry_after?: number;
98
129
  }
99
130
 
100
131
  export class Service {
101
- private readonly fetcher: Fetcher;
102
-
103
- constructor(private readonly options: IServiceOptions) {
104
- if (!options.fetcher && !globalThis.fetch) {
105
- throw new Error(
106
- 'fetch is not available in environment, pass "fetcher" option with satisfies fetch interface',
107
- );
108
- }
109
-
110
- this.fetcher = this.options.fetcher ?? globalThis.fetch;
111
- }
112
-
113
- async createCommitment(
114
- reference_id: string,
115
- amount: number,
116
- currency: string,
117
- ): Promise<ICommitment> {
118
- const response = await request<ICommitment>(
119
- buildUrl(this.options.endpoint, "/commitment"),
120
- {
121
- method: "POST",
122
- body: JSON.stringify({
123
- tenant_id: this.options.tenant_id,
124
- reference_id,
125
- currency: currency,
126
- amount,
127
- }),
128
- },
129
- this.fetcher,
130
- );
131
-
132
- if (response.error) {
133
- throw new Error(response.message);
134
- }
135
-
136
- if (!response.data) {
137
- throw new Error("Bad response");
138
- }
139
-
140
- return response.data;
141
- }
142
-
143
- async checkCommitment(commitment: string): Promise<ICommitmentStatus> {
144
- const response = await request<ICommitmentStatus>(
145
- buildUrl(this.options.endpoint, `/commitment/${commitment}`),
146
- {
147
- method: "GET",
148
- },
149
- this.fetcher,
150
- );
151
-
152
- switch (response.status) {
153
- case 402:
154
- return {
155
- commitment_id: commitment,
156
- message: response.message || "Payment required - you are over limit",
157
- status: "rejected",
158
- };
159
- case 410:
160
- return {
161
- commitment_id: commitment,
162
- message: response.message || "Expired",
163
- status: "expired",
164
- };
165
- }
166
-
167
- if (response.error) {
168
- throw new Error(response.message);
169
- }
170
-
171
- if (!response.data) {
172
- throw new Error("Bad response");
173
- }
174
-
175
- return response.data;
176
- }
132
+ private readonly fetcher: Fetcher;
133
+
134
+ constructor(private readonly options: IServiceOptions) {
135
+ if (!options.fetcher && !globalThis.fetch) {
136
+ throw new Error(
137
+ 'fetch is not available in environment, pass "fetcher" option with satisfies fetch interface',
138
+ );
139
+ }
140
+
141
+ this.fetcher = this.options.fetcher ?? globalThis.fetch;
142
+ }
143
+
144
+ async createCommitment(
145
+ reference_id: string,
146
+ amount: number,
147
+ currency: string,
148
+ metadata?: ICommitmentMetadata
149
+ ): Promise<ICommitment> {
150
+ const response = await request<ICommitment>(
151
+ buildUrl(this.options.endpoint, "/commitment"),
152
+ {
153
+ method: "POST",
154
+ body: JSON.stringify({
155
+ tenant_id: this.options.tenant_id,
156
+ reference_id,
157
+ currency: currency,
158
+ metadata: metadata ?? {title: "", url: ""},
159
+ amount,
160
+ }),
161
+ },
162
+ this.fetcher,
163
+ );
164
+
165
+ if (response.error) {
166
+ throw new Error(response.message);
167
+ }
168
+
169
+ if (!response.data) {
170
+ throw new Error("Bad response");
171
+ }
172
+
173
+ return response.data;
174
+ }
175
+
176
+ async checkCommitment(commitment: string): Promise<ICommitmentStatus> {
177
+ const response = await request<ICommitmentStatus>(
178
+ buildUrl(this.options.endpoint, `/commitment/${commitment}`),
179
+ {
180
+ method: "GET",
181
+ },
182
+ this.fetcher,
183
+ );
184
+
185
+ switch (response.status) {
186
+ case 402:
187
+ return {
188
+ commitment_id: commitment,
189
+ message: response.message || "Payment required - you are over limit",
190
+ status: "rejected",
191
+ };
192
+ case 410:
193
+ return {
194
+ commitment_id: commitment,
195
+ message: response.message || "Expired",
196
+ status: "expired",
197
+ };
198
+ }
199
+
200
+ if (response.error) {
201
+ throw new Error(response.message);
202
+ }
203
+
204
+ if (!response.data) {
205
+ throw new Error("Bad response");
206
+ }
207
+
208
+ return {
209
+ commitment_id: response.data.commitment_id,
210
+ message: response.data.message,
211
+ status: response.data.status,
212
+ retry_after: response.retry_after
213
+ };
214
+ }
177
215
  }