@lacagnottedesproches/lcdp-js-lite 1.6.0

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/README.md ADDED
@@ -0,0 +1,9 @@
1
+ [![Test with coverage afina-js-lite 🎉](https://github.com/lacagnottedesproches/afina-js-lite/actions/workflows/test.js.yml/badge.svg)](https://github.com/lacagnottedesproches/afina-js-lite/actions/workflows/test.js.yml)
2
+
3
+ [![Build and test with coverage afina-js-lite 🎉](https://github.com/lacagnottedesproches/afina-js-lite/actions/workflows/node.js.yml/badge.svg)](https://github.com/lacagnottedesproches/afina-js-lite/actions/workflows/node.js.yml)
4
+
5
+ <div align="center">
6
+ <h1>La Cagnotte des Proches - Afina JS Lite</h1>
7
+ </div>
8
+
9
+ The SDK JS, that is a wrapper around **afina-sdk**.
@@ -0,0 +1,171 @@
1
+ import fetchMock from "jest-fetch-mock";
2
+
3
+ import { LCDPRequest } from "../src/api";
4
+ import { TokenObject } from "../src/types";
5
+
6
+ const API_URL = "https://api.test.local";
7
+ const SECRET = "sk_test_123";
8
+
9
+ const mockToken: TokenObject = {
10
+ id: "tok_123",
11
+ type: "account" as any,
12
+ client_ip: "127.0.0.1",
13
+ };
14
+
15
+ describe("LCDPRequest (with jest-fetch-mock)", () => {
16
+ beforeEach(() => {
17
+ fetchMock.resetMocks();
18
+ });
19
+
20
+ test("constructor throws if window is defined", () => {
21
+ (global as any).window = {};
22
+
23
+ expect(() => new LCDPRequest(SECRET, API_URL)).toThrow(
24
+ "Must be used server side",
25
+ );
26
+
27
+ delete (global as any).window;
28
+ });
29
+
30
+ test("requestLCDPApi throws if window is defined", async () => {
31
+ const api = new LCDPRequest(SECRET, API_URL);
32
+
33
+ (global as any).window = {};
34
+
35
+ expect(() => api.requestLCDPApi("accounts", mockToken, SECRET)).toThrow(
36
+ "Must be used server side",
37
+ );
38
+
39
+ delete (global as any).window;
40
+ });
41
+
42
+ test("requestLCDPApi - success JSON", async () => {
43
+ fetchMock.mockResponseOnce(JSON.stringify({ ok: true }), { status: 200 });
44
+
45
+ const api = new LCDPRequest(SECRET, API_URL);
46
+
47
+ const res = await api.requestLCDPApi("accounts", mockToken, SECRET);
48
+
49
+ expect(fetchMock).toHaveBeenCalledWith(
50
+ `${API_URL}/accounts`,
51
+ expect.objectContaining({
52
+ method: "POST",
53
+ headers: expect.objectContaining({
54
+ Authorization: `Bearer ${SECRET}`,
55
+ }),
56
+ }),
57
+ );
58
+
59
+ expect(res).toEqual({ ok: true });
60
+ });
61
+
62
+ test("requestLCDPApi - 204 No Content", async () => {
63
+ fetchMock.mockResponseOnce("", { status: 204 });
64
+
65
+ const api = new LCDPRequest(SECRET, API_URL);
66
+
67
+ const res = await api.requestLCDPApi("accounts", mockToken, SECRET);
68
+
69
+ expect(res).toBeUndefined();
70
+ });
71
+
72
+ test("requestLCDPApi - rejects on 400", async () => {
73
+ fetchMock.mockResponseOnce(JSON.stringify({ detail: "Bad Request" }), {
74
+ status: 400,
75
+ });
76
+
77
+ const api = new LCDPRequest(SECRET, API_URL);
78
+
79
+ await expect(
80
+ api.requestLCDPApi("accounts", mockToken, SECRET),
81
+ ).rejects.toBe("Bad Request");
82
+ });
83
+
84
+ test("requestLCDPApi - rejects on 500", async () => {
85
+ fetchMock.mockResponseOnce(
86
+ JSON.stringify({ detail: "Internal server error" }),
87
+ {
88
+ status: 500,
89
+ },
90
+ );
91
+
92
+ const api = new LCDPRequest(SECRET, API_URL);
93
+
94
+ await expect(
95
+ api.requestLCDPApi("accounts", mockToken, SECRET),
96
+ ).rejects.toBe("Internal server error");
97
+ });
98
+
99
+ test("createAccount()", async () => {
100
+ fetchMock.mockResponseOnce(
101
+ JSON.stringify({
102
+ acct_id: "acc_1",
103
+ first_name: "John",
104
+ last_name: "Doe",
105
+ email: "john@test.com",
106
+ }),
107
+ { status: 200 },
108
+ );
109
+
110
+ const api = new LCDPRequest(SECRET, API_URL);
111
+ const res = await api.createAccount(mockToken);
112
+
113
+ expect(res.acct_id).toBe("acc_1");
114
+ expect(fetchMock).toHaveBeenCalledTimes(1);
115
+ });
116
+
117
+ test("createPot()", async () => {
118
+ fetchMock.mockResponseOnce(
119
+ JSON.stringify({
120
+ pot_id: "pot_1",
121
+ name: "My pot",
122
+ slug: "my-pot",
123
+ }),
124
+ { status: 200 },
125
+ );
126
+
127
+ const api = new LCDPRequest(SECRET, API_URL);
128
+ const res = await api.createPot(mockToken);
129
+
130
+ expect(res.pot_id).toBe("pot_1");
131
+ });
132
+
133
+ test("createAccountAndPot()", async () => {
134
+ fetchMock.mockResponseOnce(
135
+ JSON.stringify({
136
+ account: {
137
+ acct_id: "acc_1",
138
+ first_name: "John",
139
+ last_name: "Doe",
140
+ email: "john@test.com",
141
+ },
142
+ pot: {
143
+ pot_id: "pot_1",
144
+ name: "My pot",
145
+ slug: "my-pot",
146
+ },
147
+ }),
148
+ { status: 200 },
149
+ );
150
+
151
+ const api = new LCDPRequest(SECRET, API_URL);
152
+ const res = await api.createAccountAndPot(mockToken);
153
+
154
+ expect(res.account.acct_id).toBe("acc_1");
155
+ expect(res.pot.pot_id).toBe("pot_1");
156
+ });
157
+
158
+ test("updatePot() uses PATCH and resolves void", async () => {
159
+ fetchMock.mockResponseOnce("", { status: 204 });
160
+
161
+ const api = new LCDPRequest(SECRET, API_URL);
162
+ await api.updatePot(mockToken);
163
+
164
+ expect(fetchMock).toHaveBeenCalledWith(
165
+ `${API_URL}/pots`,
166
+ expect.objectContaining({
167
+ method: "PATCH",
168
+ }),
169
+ );
170
+ });
171
+ });
@@ -0,0 +1,228 @@
1
+ import { loadLCDP, requestLCDP } from "../src";
2
+ import { LCDPRequest } from "../src/api";
3
+
4
+ describe("index.ts in node env", () => {
5
+ beforeEach(() => {
6
+ jest.clearAllMocks();
7
+
8
+ // Reset window between tests
9
+ (globalThis as any).window = {};
10
+ (globalThis as any).window.lcdp = undefined;
11
+ });
12
+
13
+ it("resolves null if window is missing", async () => {
14
+ const originalWindow = (globalThis as any).window;
15
+ const originalDocument = (globalThis as any).document;
16
+
17
+ delete (globalThis as any).window;
18
+ delete (globalThis as any).document;
19
+
20
+ const result = await loadLCDP("key");
21
+
22
+ expect(result).toBeNull();
23
+
24
+ // Restore
25
+ (globalThis as any).window = originalWindow;
26
+ (globalThis as any).document = originalDocument;
27
+ });
28
+
29
+ it("uses existing window.lcdp.Afina", async () => {
30
+ jest.resetModules();
31
+
32
+ const initMock = jest.fn().mockResolvedValue({ ready: true });
33
+
34
+ (globalThis as any).window.lcdp = {
35
+ Afina: function (...args: any[]) {
36
+ (this as any).init = initMock;
37
+ },
38
+ };
39
+
40
+ const { loadLCDP } = await import("../src");
41
+ const result = await loadLCDP("key");
42
+
43
+ expect(initMock).toHaveBeenCalled();
44
+ expect(result).toEqual({ ready: true });
45
+ });
46
+
47
+ it("injects script if not present", async () => {
48
+ jest.resetModules(); // reset module-level state
49
+
50
+ const initMock = jest.fn().mockResolvedValue({ ok: true });
51
+
52
+ // recreate window before importing
53
+ (globalThis as any).window = {};
54
+ (globalThis as any).document = {
55
+ head: { appendChild: jest.fn() },
56
+ body: { appendChild: jest.fn() },
57
+ createElement: jest.fn((tag: string) => {
58
+ if (tag === "script") {
59
+ return {
60
+ src: "",
61
+ addEventListener: jest.fn((event, cb) => {
62
+ // automatically call 'load' after a tick
63
+ if (event === "load") {
64
+ setTimeout(() => {
65
+ // mock window.lcdp only when load triggers
66
+ (globalThis as any).window = {};
67
+ (globalThis as any).window.lcdp = {
68
+ Afina: function () {
69
+ this.init = initMock;
70
+ },
71
+ };
72
+ cb();
73
+ }, 0);
74
+ }
75
+ }),
76
+ removeEventListener: jest.fn(),
77
+ parentNode: { removeChild: jest.fn() },
78
+ };
79
+ }
80
+ return { appendChild: jest.fn(), id: "" };
81
+ }),
82
+ querySelectorAll: jest.fn(() => []),
83
+ };
84
+
85
+ // import fresh module after window/document mocks
86
+ const { loadLCDP } = await import("../src");
87
+
88
+ const result = await loadLCDP("key");
89
+
90
+ expect(initMock).toHaveBeenCalled();
91
+ expect(result).toEqual({ ok: true });
92
+ });
93
+
94
+ it("does not injects script if already present", async () => {
95
+ jest.resetModules(); // reset module-level state
96
+
97
+ const initMock = jest.fn().mockResolvedValue({ ok: true });
98
+
99
+ // recreate window before importing
100
+ (globalThis as any).window = {};
101
+ (globalThis as any).document = {
102
+ head: { appendChild: jest.fn() },
103
+ body: { appendChild: jest.fn() },
104
+ createElement: jest.fn((tag: string) => {
105
+ if (tag === "script") {
106
+ return {
107
+ src: "",
108
+ addEventListener: jest.fn((event, cb) => {
109
+ // automatically call 'load' after a tick
110
+ if (event === "load") {
111
+ setTimeout(() => {
112
+ // mock window.lcdp only when load triggers
113
+ (globalThis as any).window = {};
114
+ (globalThis as any).window.lcdp = {
115
+ Afina: function () {
116
+ this.init = initMock;
117
+ },
118
+ };
119
+ cb();
120
+ }, 0);
121
+ }
122
+ }),
123
+ removeEventListener: jest.fn(),
124
+ parentNode: { removeChild: jest.fn() },
125
+ };
126
+ }
127
+ return { appendChild: jest.fn(), id: "" };
128
+ }),
129
+ querySelectorAll: jest.fn(() => [
130
+ {
131
+ src: "",
132
+ addEventListener: jest.fn((event, cb) => {
133
+ // automatically call 'load' after a tick
134
+ if (event === "load") {
135
+ setTimeout(() => {
136
+ // mock window.lcdp only when load triggers
137
+ (globalThis as any).window = {};
138
+ (globalThis as any).window.lcdp = {
139
+ Afina: function () {
140
+ this.init = initMock;
141
+ },
142
+ };
143
+ cb();
144
+ }, 0);
145
+ }
146
+ }),
147
+ removeEventListener: jest.fn(),
148
+ parentNode: { removeChild: jest.fn() },
149
+ },
150
+ ]),
151
+ };
152
+
153
+ // import fresh module after window/document mocks
154
+ const { loadLCDP } = await import("../src");
155
+
156
+ const result = await loadLCDP("key");
157
+
158
+ expect(initMock).toHaveBeenCalled();
159
+ expect(result).toEqual({ ok: true });
160
+ });
161
+
162
+ it("rejects when script fails to load", async () => {
163
+ jest.resetModules(); // reset module-level state
164
+
165
+ const initMock = jest.fn().mockResolvedValue({ ok: true });
166
+
167
+ // recreate window before importing
168
+ (globalThis as any).window = {};
169
+ (globalThis as any).document = {
170
+ head: { appendChild: jest.fn() },
171
+ body: { appendChild: jest.fn() },
172
+ createElement: jest.fn((tag: string) => {
173
+ if (tag === "script") {
174
+ return {
175
+ src: "",
176
+ addEventListener: jest.fn((event, cb) => {
177
+ // automatically call 'load' after a tick
178
+ if (event === "load") {
179
+ setTimeout(() => {
180
+ // mock window.lcdp only when load triggers
181
+ (globalThis as any).window = {};
182
+ (globalThis as any).window.lcdp = {
183
+ Afina: function () {
184
+ this.init = initMock;
185
+ },
186
+ };
187
+ cb();
188
+ }, 0);
189
+ }
190
+ }),
191
+ removeEventListener: jest.fn(),
192
+ parentNode: { removeChild: jest.fn() },
193
+ };
194
+ }
195
+ return { appendChild: jest.fn(), id: "" };
196
+ }),
197
+ querySelectorAll: jest.fn(() => []),
198
+ };
199
+
200
+ // import fresh module after window/document mocks
201
+ const { loadLCDP } = await import("../src");
202
+
203
+ const promise = loadLCDP("key");
204
+
205
+ const mockScript = (document.createElement as jest.Mock).mock.results[0]
206
+ .value;
207
+
208
+ const errorHandler = mockScript.addEventListener.mock.calls.find(
209
+ (c: any) => c[0] === "error",
210
+ )[1];
211
+
212
+ errorHandler();
213
+
214
+ await expect(promise).rejects.toThrow("Failed to load lcdp.js");
215
+ });
216
+
217
+ it("requestLCDP returns LCDPRequest", () => {
218
+ jest.clearAllMocks();
219
+
220
+ // Reset window between tests
221
+ (globalThis as any).window = undefined;
222
+
223
+ const client = requestLCDP("secret");
224
+
225
+ expect(client).toBeInstanceOf(LCDPRequest);
226
+ expect(client._apiToken).toBe("secret");
227
+ });
228
+ });
@@ -0,0 +1,16 @@
1
+ import { AfinaError, TokenObjectType } from "../src/types";
2
+
3
+ describe("Runtime enums", () => {
4
+ test("AfinaError values", () => {
5
+ expect(AfinaError.Verify).toBe("Fail to verify origin request");
6
+ expect(AfinaError.Stripe).toBe("Stripe is not available");
7
+ expect(AfinaError.ClientSide).toBe("Should be on Client Side");
8
+ expect(AfinaError.Key).toBe("A publishable key is mandatory");
9
+ });
10
+
11
+ test("TokenObjectType values", () => {
12
+ expect(TokenObjectType.Account).toBe("account");
13
+ expect(TokenObjectType.Pot).toBe("pot");
14
+ expect(TokenObjectType.Wizard).toBe("wizard");
15
+ });
16
+ });
@@ -0,0 +1,87 @@
1
+ import { expectType } from "tsd";
2
+ import {
3
+ Pot,
4
+ PayloadAccount,
5
+ DonationResult,
6
+ TokenObject,
7
+ ParamsPagination,
8
+ LCDP,
9
+ TokenObjectType,
10
+ } from "../src/types";
11
+
12
+ // ---- Pot ----
13
+ expectType<Pot>({
14
+ id: "123",
15
+ date_created_ts: 1234567890,
16
+ slug: "slug",
17
+ name: "Pot name",
18
+ date_end_ts: null,
19
+ objectif: null,
20
+ status_online: true,
21
+ status_public: true,
22
+ status_closed: false,
23
+ amount_collected_hidden: false,
24
+ public_pot_url: "url",
25
+ public_donation_url: "url",
26
+ stat_amount_donation: null,
27
+ stat_nb_donation: 0,
28
+ profit_acct: null,
29
+ profit_ghost: null,
30
+ description: null,
31
+ video: null,
32
+ visual_main_url: null,
33
+ visual_secondary_url: null,
34
+ with_tax_receipt: false,
35
+ });
36
+
37
+ // ---- PayloadAccount ----
38
+ expectType<PayloadAccount>({
39
+ email: "test@test.com",
40
+ first_name: "John",
41
+ last_name: "Doe",
42
+ birth_date: new Date(),
43
+ address: "Street",
44
+ postcode: "75000",
45
+ city: "Paris",
46
+ phone_number: "0600000000",
47
+ country_code: "FR",
48
+ });
49
+
50
+ // ---- DonationResult ----
51
+ expectType<DonationResult>({
52
+ status: "requires_action",
53
+ client_secret: "sec_123",
54
+ });
55
+
56
+ // ---- TokenObject ----
57
+ expectType<TokenObject>({
58
+ id: "tok_123",
59
+ type: TokenObjectType.Account,
60
+ client_ip: "127.0.0.1",
61
+ });
62
+
63
+ // ---- ParamsPagination ----
64
+ expectType<ParamsPagination>({
65
+ page: 1,
66
+ per_page: 10,
67
+ });
68
+
69
+ // ---- LCDP shape ----
70
+ expectType<LCDP>({
71
+ stripe: null,
72
+ init: async () => ({}) as any,
73
+ createTokenAccount: async () => ({}) as any,
74
+ createTokenPot: async () => ({}) as any,
75
+ createTokenAccountAndPot: async () => ({}) as any,
76
+ createTokenPotUpdate: async () => ({}) as any,
77
+ createDonation: async () => ({}) as any,
78
+ getAvailableCountries: async () => [],
79
+ getCategories: async () => [],
80
+ getPots: async () => ({}) as any,
81
+ getPot: async () => ({}) as any,
82
+ getStatsDashboard: async () => ({}) as any,
83
+ getStatsPots: async () => ({}) as any,
84
+ retrieveAssociations: async () => ({}) as any,
85
+ getAssociation: async () => ({}) as any,
86
+ getAssociationCategories: async () => [],
87
+ });
package/dist/api.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { AccountCreated, LCDPApi, PotCreated, TokenObject, AccountAndPotCreated } from "./types";
2
+ interface LCDPApiResponse {
3
+ [key: string]: string | LCDPApiResponse;
4
+ }
5
+ export declare class LCDPRequest implements LCDPApi {
6
+ _apiToken: string;
7
+ _apiUrl: string;
8
+ constructor(secret: string, apiUrl: string);
9
+ requestLCDPApi: (endpoint: string, payload: TokenObject, authToken: string, customMethod?: "POST" | "PATCH") => Promise<LCDPApiResponse | void>;
10
+ createAccount: (payload: TokenObject) => Promise<AccountCreated>;
11
+ createPot: (payload: TokenObject) => Promise<PotCreated>;
12
+ createAccountAndPot: (payload: TokenObject) => Promise<AccountAndPotCreated>;
13
+ updatePot: (payload: TokenObject) => Promise<void>;
14
+ }
15
+ export {};
@@ -0,0 +1,5 @@
1
+ import { LCDPRequest } from "./api";
2
+ import { LoadLCDP } from "./types";
3
+ export * from "./types";
4
+ export declare const loadLCDP: LoadLCDP;
5
+ export declare const requestLCDP: (secret: string, apiUrl?: string) => LCDPRequest;