@gr4vy/sdk 2.1.1 → 2.1.3

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 (46) hide show
  1. package/README.md +8 -2
  2. package/jsr.json +1 -1
  3. package/lib/config.d.ts +2 -2
  4. package/lib/config.js +2 -2
  5. package/models/components/method.d.ts +2 -0
  6. package/models/components/method.d.ts.map +1 -1
  7. package/models/components/method.js +2 -0
  8. package/models/components/method.js.map +1 -1
  9. package/models/components/redirectpaymentmethodcreate.d.ts +2 -0
  10. package/models/components/redirectpaymentmethodcreate.d.ts.map +1 -1
  11. package/models/components/redirectpaymentmethodcreate.js +2 -0
  12. package/models/components/redirectpaymentmethodcreate.js.map +1 -1
  13. package/models/components/transactionconnectionoptions.d.ts +10 -0
  14. package/models/components/transactionconnectionoptions.d.ts.map +1 -1
  15. package/models/components/transactionconnectionoptions.js +4 -0
  16. package/models/components/transactionconnectionoptions.js.map +1 -1
  17. package/package.json +4 -2
  18. package/scripts/endpoint-coverage.mjs +136 -0
  19. package/src/lib/config.ts +2 -2
  20. package/src/models/components/method.ts +2 -0
  21. package/src/models/components/redirectpaymentmethodcreate.ts +2 -0
  22. package/src/models/components/transactionconnectionoptions.ts +14 -0
  23. package/tests/backoffice/account-updater.test.ts +24 -0
  24. package/tests/backoffice/audit-logs.test.ts +20 -0
  25. package/tests/backoffice/merchant-accounts.test.ts +77 -0
  26. package/tests/backoffice/payment-services.test.ts +95 -0
  27. package/tests/backoffice/payouts.test.ts +49 -0
  28. package/tests/backoffice/reports.test.ts +69 -0
  29. package/tests/backoffice/three-ds-scenarios.test.ts +34 -0
  30. package/tests/flows/buyer-lifecycle.test.ts +74 -0
  31. package/tests/flows/transaction-lifecycle.test.ts +151 -0
  32. package/tests/processing/buyers.test.ts +119 -0
  33. package/tests/processing/checkout-sessions.test.ts +99 -0
  34. package/tests/processing/digital-wallets.test.ts +154 -0
  35. package/tests/processing/gift-cards.test.ts +45 -0
  36. package/tests/processing/payment-links.test.ts +34 -0
  37. package/tests/processing/payment-methods.test.ts +121 -0
  38. package/tests/processing/transactions.test.ts +118 -0
  39. package/tests/utils/arbitraries.ts +87 -0
  40. package/tests/utils/fields.ts +65 -0
  41. package/tests/utils/fixtures.ts +84 -0
  42. package/tests/utils/poll.ts +40 -0
  43. package/tests/utils/setup.ts +66 -10
  44. package/tests/utils/transactions.ts +26 -0
  45. package/vitest.config.ts +31 -0
  46. package/tests/checkout-sessions.test.ts +0 -109
@@ -0,0 +1,99 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { CardPaymentMethodCreate } from "../../src/models/components";
4
+ import {
5
+ APPROVING_CARD,
6
+ cardPaymentMethod,
7
+ cartItem,
8
+ } from "../utils/fixtures";
9
+ import {
10
+ putCheckoutSessionCard,
11
+ putCheckoutSessionStoredMethod,
12
+ } from "../utils/fields";
13
+ import { setupMerchant } from "../utils/setup";
14
+
15
+ let gr4vy: Gr4vy;
16
+
17
+ beforeAll(async () => {
18
+ ({ client: gr4vy } = await setupMerchant());
19
+ });
20
+
21
+ describe("Checkout Sessions", () => {
22
+ test("should process a payment with a checkout session", async () => {
23
+ const checkoutSession = await gr4vy.checkoutSessions.create();
24
+ expect(checkoutSession.id).toBeDefined();
25
+
26
+ await putCheckoutSessionCard(checkoutSession, APPROVING_CARD);
27
+
28
+ const transaction = await gr4vy.transactions.create({
29
+ amount: 1299,
30
+ currency: "USD",
31
+ paymentMethod: { method: "checkout-session", id: checkoutSession.id },
32
+ });
33
+
34
+ expect(transaction.id).toBeDefined();
35
+ expect(transaction.status).toBe("authorization_succeeded");
36
+ expect(transaction.amount).toBe(1299);
37
+ });
38
+
39
+ test("should handle the error raised on missing card data", async () => {
40
+ const checkoutSession = await gr4vy.checkoutSessions.create();
41
+ expect(checkoutSession.id).toBeDefined();
42
+
43
+ const request = {
44
+ amount: 1299,
45
+ currency: "USD",
46
+ paymentMethod: {
47
+ method: "checkout-session" as const,
48
+ id: checkoutSession.id,
49
+ },
50
+ };
51
+ await expect(() => gr4vy.transactions.create(request)).rejects.toThrowError(
52
+ "Request failed validation"
53
+ );
54
+ });
55
+
56
+ test("should handle a stored payment method", async () => {
57
+ const request: CardPaymentMethodCreate = cardPaymentMethod();
58
+ const paymentMethod = await gr4vy.paymentMethods.create(request);
59
+ expect(paymentMethod.id).toBeDefined();
60
+
61
+ const checkoutSession = await gr4vy.checkoutSessions.create();
62
+ await putCheckoutSessionStoredMethod(
63
+ checkoutSession,
64
+ paymentMethod.id,
65
+ "123"
66
+ );
67
+
68
+ const transaction = await gr4vy.transactions.create({
69
+ amount: 1299,
70
+ currency: "USD",
71
+ paymentMethod: { method: "checkout-session", id: checkoutSession.id },
72
+ });
73
+
74
+ expect(transaction.id).toBeDefined();
75
+ expect(transaction.status).toBe("authorization_succeeded");
76
+ expect(transaction.amount).toBe(1299);
77
+ });
78
+
79
+ test("create → get → update (partial) → delete", async () => {
80
+ const created = await gr4vy.checkoutSessions.create({
81
+ cartItems: [cartItem()],
82
+ metadata: { source: "test" },
83
+ });
84
+ expect(created.id).toBeDefined();
85
+
86
+ const fetched = await gr4vy.checkoutSessions.get(created.id);
87
+ expect(fetched.id).toBe(created.id);
88
+
89
+ // Partial update: change only the metadata, leave the cart items.
90
+ const updated = await gr4vy.checkoutSessions.update(
91
+ { metadata: { source: "updated" } },
92
+ created.id
93
+ );
94
+ expect(updated.metadata?.["source"]).toBe("updated");
95
+
96
+ await gr4vy.checkoutSessions.delete(created.id);
97
+ await expect(() => gr4vy.checkoutSessions.get(created.id)).rejects.toThrow();
98
+ });
99
+ });
@@ -0,0 +1,154 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { DigitalWallet } from "../../src/models/components";
4
+ import { uniqueId } from "../utils/fixtures";
5
+ import { setupMerchant } from "../utils/setup";
6
+
7
+ let gr4vy: Gr4vy;
8
+
9
+ beforeAll(async () => {
10
+ ({ client: gr4vy } = await setupMerchant());
11
+ });
12
+
13
+ const registerGoogleWallet = () =>
14
+ gr4vy.digitalWallets.create({
15
+ provider: "google",
16
+ merchantName: uniqueId("gr4vy-e2e"),
17
+ merchantDisplayName: "Gr4vy E2E",
18
+ acceptTermsAndConditions: true,
19
+ });
20
+
21
+ // A merchant allows only one wallet per provider, so register a wallet, run the
22
+ // callback against it, then always remove it — keeping the tests isolated even
23
+ // though they share a merchant account.
24
+ const withWallet = async (
25
+ fn: (wallet: DigitalWallet) => Promise<void>
26
+ ): Promise<void> => {
27
+ const wallet = await registerGoogleWallet();
28
+ try {
29
+ await fn(wallet);
30
+ } finally {
31
+ await gr4vy.digitalWallets.delete(wallet.id);
32
+ }
33
+ };
34
+
35
+ describe("Digital Wallets", () => {
36
+ test("register → get → list → update (partial) → delete", async () => {
37
+ const created = await registerGoogleWallet();
38
+ expect(created.id).toBeDefined();
39
+
40
+ const fetched = await gr4vy.digitalWallets.get(created.id);
41
+ expect(fetched.id).toBe(created.id);
42
+
43
+ const list = await gr4vy.digitalWallets.list();
44
+ expect(list.items.some((w) => w.id === created.id)).toBe(true);
45
+
46
+ // Partial update: change only the display name.
47
+ const updated = await gr4vy.digitalWallets.update(
48
+ { merchantDisplayName: "Renamed Display" },
49
+ created.id
50
+ );
51
+ expect(updated.merchantDisplayName).toBe("Renamed Display");
52
+
53
+ await gr4vy.digitalWallets.delete(created.id);
54
+ await expect(() => gr4vy.digitalWallets.get(created.id)).rejects.toThrow();
55
+ });
56
+
57
+ // A Google Pay session can be minted server-side from an origin domain; assert
58
+ // the SDK returns the gateway merchant id + session token.
59
+ test("creates a google pay session", async () => {
60
+ const session = await gr4vy.digitalWallets.sessions.googlePay({
61
+ originDomain: "not-a-registered-domain.example.com",
62
+ });
63
+ expect(session).toHaveProperty("token");
64
+ });
65
+
66
+ // The remaining wallet sessions (Apple Pay, Click to Pay, Paze + Paze mobile)
67
+ // need a verified wallet / real device payloads that can't be minted on the
68
+ // mock merchant. Each is exercised at the request level so the SDK request
69
+ // shape is covered and the API rejects it for a real reason.
70
+ describe("sessions exercised at the request level", () => {
71
+ test("apple pay session", async () => {
72
+ await expect(
73
+ gr4vy.digitalWallets.sessions.applePay({
74
+ validationUrl: "https://apple-pay-gateway.apple.com/paymentservices",
75
+ domainName: "not-a-registered-domain.example.com",
76
+ })
77
+ ).rejects.toThrow();
78
+ });
79
+
80
+ test("click to pay session", async () => {
81
+ await expect(
82
+ gr4vy.digitalWallets.sessions.clickToPay({
83
+ checkoutSessionId: "00000000-0000-0000-0000-000000000000",
84
+ })
85
+ ).rejects.toThrow();
86
+ });
87
+
88
+ test("paze session", async () => {
89
+ await expect(
90
+ gr4vy.digitalWallets.sessions.paze({
91
+ source: "web",
92
+ domainName: "not-a-registered-domain.example.com",
93
+ })
94
+ ).rejects.toThrow();
95
+ });
96
+
97
+ test("paze mobile session create / review / complete", async () => {
98
+ await expect(
99
+ gr4vy.digitalWallets.sessions.pazeMobileSessionCreate({
100
+ client: { id: "client-id" },
101
+ sessionId: "session-id",
102
+ accessToken: "access-token",
103
+ callbackURLScheme: "gr4vy",
104
+ intent: "EXPRESS_CHECKOUT",
105
+ })
106
+ ).rejects.toThrow();
107
+
108
+ await expect(
109
+ gr4vy.digitalWallets.sessions.pazeMobileSessionReview({
110
+ sessionId: "session-id",
111
+ code: "code",
112
+ accessToken: "access-token",
113
+ })
114
+ ).rejects.toThrow();
115
+
116
+ await expect(
117
+ gr4vy.digitalWallets.sessions.pazeMobileSessionComplete({
118
+ sessionId: "session-id",
119
+ code: "code",
120
+ accessToken: "access-token",
121
+ transactionType: "PURCHASE",
122
+ })
123
+ ).rejects.toThrow();
124
+ });
125
+ });
126
+
127
+ // Apple Pay domain registration needs an Apple-provider wallet onboarded with
128
+ // Apple; on the mock merchant both register and remove are rejected, which
129
+ // still exercises the domain create/delete request shapes.
130
+ describe("domains exercised at the request level", () => {
131
+ test("domain registration is rejected", async () => {
132
+ await withWallet(async (wallet) => {
133
+ await expect(
134
+ gr4vy.digitalWallets.domains.create(
135
+ { domainName: "example.com" },
136
+ wallet.id
137
+ )
138
+ ).rejects.toThrow();
139
+ });
140
+ });
141
+
142
+ test("domain removal is idempotent", async () => {
143
+ await withWallet(async (wallet) => {
144
+ // Removing a domain that was never registered resolves without error.
145
+ await expect(
146
+ gr4vy.digitalWallets.domains.delete(
147
+ { domainName: "example.com" },
148
+ wallet.id
149
+ )
150
+ ).resolves.toBeUndefined();
151
+ });
152
+ });
153
+ });
154
+ });
@@ -0,0 +1,45 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { setupMerchant } from "../utils/setup";
4
+
5
+ let gr4vy: Gr4vy;
6
+
7
+ beforeAll(async () => {
8
+ ({ client: gr4vy } = await setupMerchant());
9
+ });
10
+
11
+ describe("Gift Cards", () => {
12
+ test("listing gift cards returns a page", async () => {
13
+ const page = await gr4vy.giftCards.list({});
14
+ expect(page).toBeDefined();
15
+ });
16
+
17
+ // Creating a gift card and reading its balance requires a gift-card service
18
+ // (e.g. `mock-gift-card`) to be configured on the merchant. The mock merchant
19
+ // is provisioned only with `mock-card`, so these calls are exercised at the
20
+ // request level and the API is expected to reject them for a real reason.
21
+ test("create is exercised at the request level", async () => {
22
+ await expect(
23
+ gr4vy.giftCards.create({
24
+ number: "4111111111111111",
25
+ pin: "1234",
26
+ })
27
+ ).rejects.toThrow();
28
+ });
29
+
30
+ test("balance lookup is exercised at the request level", async () => {
31
+ await expect(
32
+ gr4vy.giftCards.balances.list({
33
+ items: [{ number: "4111111111111111", pin: "1234" }],
34
+ })
35
+ ).rejects.toThrow();
36
+ });
37
+
38
+ // No gift card exists to fetch/delete on the mock merchant, so get/delete are
39
+ // exercised against a non-existent id and expected to be rejected.
40
+ test("get and delete are exercised at the request level", async () => {
41
+ const bogus = "00000000-0000-0000-0000-000000000000";
42
+ await expect(gr4vy.giftCards.get(bogus)).rejects.toThrow();
43
+ await expect(gr4vy.giftCards.delete(bogus)).rejects.toThrow();
44
+ });
45
+ });
@@ -0,0 +1,34 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { cartItem, uniqueId } from "../utils/fixtures";
4
+ import { setupMerchant } from "../utils/setup";
5
+
6
+ let gr4vy: Gr4vy;
7
+
8
+ beforeAll(async () => {
9
+ ({ client: gr4vy } = await setupMerchant());
10
+ });
11
+
12
+ describe("Payment Links", () => {
13
+ test("create → get → list → expire", async () => {
14
+ const created = await gr4vy.paymentLinks.create({
15
+ amount: 1299,
16
+ currency: "USD",
17
+ country: "US",
18
+ externalIdentifier: uniqueId("plink"),
19
+ cartItems: [cartItem()],
20
+ merchantName: "Gr4vy E2E",
21
+ });
22
+ expect(created.id).toBeDefined();
23
+
24
+ const fetched = await gr4vy.paymentLinks.get(created.id);
25
+ expect(fetched.id).toBe(created.id);
26
+
27
+ const page = await gr4vy.paymentLinks.list();
28
+ expect(page).toBeDefined();
29
+
30
+ await gr4vy.paymentLinks.expire(created.id);
31
+ const afterExpiry = await gr4vy.paymentLinks.get(created.id);
32
+ expect(afterExpiry.status).toBe("expired");
33
+ });
34
+ });
@@ -0,0 +1,121 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { buyer, cardPaymentMethod } from "../utils/fixtures";
4
+ import { setupMerchant } from "../utils/setup";
5
+
6
+ let gr4vy: Gr4vy;
7
+
8
+ beforeAll(async () => {
9
+ ({ client: gr4vy } = await setupMerchant());
10
+ });
11
+
12
+ const storeCard = () => gr4vy.paymentMethods.create(cardPaymentMethod());
13
+
14
+ describe("Payment Methods", () => {
15
+ test("create → get → list → delete", async () => {
16
+ const created = await storeCard();
17
+ expect(created.id).toBeDefined();
18
+ expect(created.method).toBe("card");
19
+
20
+ const fetched = await gr4vy.paymentMethods.get(created.id);
21
+ expect(fetched.id).toBe(created.id);
22
+
23
+ const page = await gr4vy.paymentMethods.list({});
24
+ expect(page).toBeDefined();
25
+
26
+ await gr4vy.paymentMethods.delete(created.id);
27
+ await expect(() => gr4vy.paymentMethods.get(created.id)).rejects.toThrow();
28
+ });
29
+
30
+ test("update changes only the expiration date (partial)", async () => {
31
+ const created = await storeCard();
32
+ const updated = await gr4vy.paymentMethods.update(
33
+ { expirationDate: "12/30" },
34
+ created.id
35
+ );
36
+ expect(updated.id).toBe(created.id);
37
+ expect(updated.expirationDate).toBe("12/30");
38
+ });
39
+
40
+ test("can be stored against a buyer", async () => {
41
+ const owner = await gr4vy.buyers.create(buyer());
42
+ const method = await gr4vy.paymentMethods.create({
43
+ ...cardPaymentMethod(),
44
+ buyerId: owner.id,
45
+ });
46
+ expect(method.buyer?.id).toBe(owner.id);
47
+ });
48
+
49
+ describe("network tokens", () => {
50
+ test("listing tokens for a fresh card returns an empty collection", async () => {
51
+ const card = await storeCard();
52
+ const tokens = await gr4vy.paymentMethods.networkTokens.list(card.id);
53
+ expect(Array.isArray(tokens.items)).toBe(true);
54
+ });
55
+
56
+ // Provisioning a network token (and the suspend/resume/delete/cryptogram
57
+ // operations that follow it) requires the merchant account to be onboarded
58
+ // with the card schemes (Visa/Mastercard requestor IDs) — not available on
59
+ // the mock merchant. Each call is asserted to be well-formed and rejected by
60
+ // the API for a real reason rather than silently skipping coverage.
61
+ test("provision / suspend / resume / delete / cryptogram are exercised at the request level", async () => {
62
+ const card = await storeCard();
63
+ const bogusToken = "00000000-0000-0000-0000-000000000000";
64
+
65
+ await expect(
66
+ gr4vy.paymentMethods.networkTokens.create(
67
+ { merchantInitiated: true, isSubsequentPayment: false },
68
+ card.id
69
+ )
70
+ ).rejects.toThrow();
71
+
72
+ await expect(
73
+ gr4vy.paymentMethods.networkTokens.suspend(card.id, bogusToken)
74
+ ).rejects.toThrow();
75
+ await expect(
76
+ gr4vy.paymentMethods.networkTokens.resume(card.id, bogusToken)
77
+ ).rejects.toThrow();
78
+ await expect(
79
+ gr4vy.paymentMethods.networkTokens.delete(card.id, bogusToken)
80
+ ).rejects.toThrow();
81
+ await expect(
82
+ gr4vy.paymentMethods.networkTokens.cryptogram.create(
83
+ { merchantInitiated: true },
84
+ card.id,
85
+ bogusToken
86
+ )
87
+ ).rejects.toThrow();
88
+ });
89
+ });
90
+
91
+ describe("payment service tokens", () => {
92
+ test("listing service tokens for a fresh card returns a collection", async () => {
93
+ const card = await storeCard();
94
+ const tokens = await gr4vy.paymentMethods.paymentServiceTokens.list(
95
+ card.id
96
+ );
97
+ expect(Array.isArray(tokens.items)).toBe(true);
98
+ });
99
+
100
+ // Creating/deleting a service token requires a tokenization-capable service,
101
+ // which mock-card is not; exercise the call shape at the request level.
102
+ test("create and delete are exercised at the request level", async () => {
103
+ const card = await storeCard();
104
+ await expect(
105
+ gr4vy.paymentMethods.paymentServiceTokens.create(
106
+ {
107
+ paymentServiceId: "00000000-0000-0000-0000-000000000000",
108
+ redirectUrl: "https://example.com/return",
109
+ },
110
+ card.id
111
+ )
112
+ ).rejects.toThrow();
113
+ await expect(
114
+ gr4vy.paymentMethods.paymentServiceTokens.delete(
115
+ card.id,
116
+ "00000000-0000-0000-0000-000000000000"
117
+ )
118
+ ).rejects.toThrow();
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,118 @@
1
+ import fc from "fast-check";
2
+ import { beforeAll, describe, expect, test } from "vitest";
3
+ import { Gr4vy } from "../../src";
4
+ import { amount, currency, fcParams, metadata } from "../utils/arbitraries";
5
+ import { buyer, cardPaymentMethod, uniqueId } from "../utils/fixtures";
6
+ import { authorizeViaCheckoutSession } from "../utils/transactions";
7
+ import { setupMerchant } from "../utils/setup";
8
+
9
+ let gr4vy: Gr4vy;
10
+
11
+ beforeAll(async () => {
12
+ ({ client: gr4vy } = await setupMerchant());
13
+ });
14
+
15
+ // There are two ways to start processing a payment:
16
+ // 1. via a checkout session (covered throughout — see the lifecycle flow and
17
+ // the helper authorizeViaCheckoutSession), and
18
+ // 2. by posting payment-method details directly to POST /transactions.
19
+ // This block covers entry point (2) — both raw card details and a stored token.
20
+ describe("Transaction entry points", () => {
21
+ test("creates a transaction with card details posted directly", async () => {
22
+ const transaction = await gr4vy.transactions.create({
23
+ amount: 1299,
24
+ currency: "USD",
25
+ externalIdentifier: uniqueId("txn"),
26
+ paymentMethod: cardPaymentMethod(),
27
+ });
28
+ expect(transaction.status).toBe("authorization_succeeded");
29
+ expect(transaction.amount).toBe(1299);
30
+ });
31
+
32
+ test("direct card details can be vaulted against a buyer with store=true", async () => {
33
+ const owner = await gr4vy.buyers.create(buyer());
34
+
35
+ const transaction = await gr4vy.transactions.create({
36
+ amount: 1299,
37
+ currency: "USD",
38
+ buyerId: owner.id,
39
+ store: true,
40
+ externalIdentifier: uniqueId("txn"),
41
+ paymentMethod: cardPaymentMethod(),
42
+ });
43
+ expect(transaction.status).toBe("authorization_succeeded");
44
+
45
+ // The vaulted method is now reusable as a stored token.
46
+ const stored = await gr4vy.buyers.paymentMethods.list({ buyerId: owner.id });
47
+ expect(stored.items.length).toBeGreaterThanOrEqual(1);
48
+
49
+ const reused = await gr4vy.transactions.create({
50
+ amount: 500,
51
+ currency: "USD",
52
+ externalIdentifier: uniqueId("txn"),
53
+ paymentMethod: { method: "id", id: stored.items[0]!.id, securityCode: "123" },
54
+ });
55
+ expect(reused.status).toBe("authorization_succeeded");
56
+ });
57
+ });
58
+
59
+ describe("Transactions", () => {
60
+ test("get and list reflect a created transaction", async () => {
61
+ const created = await authorizeViaCheckoutSession(gr4vy, 1299);
62
+
63
+ const fetched = await gr4vy.transactions.get(created.id);
64
+ expect(fetched.id).toBe(created.id);
65
+
66
+ const page = await gr4vy.transactions.list({});
67
+ expect(page).toBeDefined();
68
+ });
69
+
70
+ test("update overrides metadata and external identifier (partial)", async () => {
71
+ const created = await authorizeViaCheckoutSession(gr4vy, 1299, {
72
+ metadata: { original: "value" },
73
+ });
74
+
75
+ const newExternalIdentifier = uniqueId("txn-updated");
76
+ const updated = await gr4vy.transactions.update(
77
+ {
78
+ metadata: { updated: "value" },
79
+ externalIdentifier: newExternalIdentifier,
80
+ },
81
+ created.id
82
+ );
83
+ expect(updated.metadata?.["updated"]).toBe("value");
84
+ // metadata is fully replaced on update, per the API contract.
85
+ expect(updated.metadata?.["original"]).toBeUndefined();
86
+ expect(updated.externalIdentifier).toBe(newExternalIdentifier);
87
+ });
88
+
89
+ // Property: any accepted amount/currency authorises and the response echoes
90
+ // the amount and currency we sent.
91
+ test("authorizes across a range of amounts and echoes them back", async () => {
92
+ await fc.assert(
93
+ fc.asyncProperty(amount(), currency(), async (amt, cur) => {
94
+ const transaction = await authorizeViaCheckoutSession(gr4vy, amt, {
95
+ currency: cur,
96
+ });
97
+ expect(transaction.status).toBe("authorization_succeeded");
98
+ expect(transaction.amount).toBe(amt);
99
+ expect(transaction.currency).toBe(cur);
100
+ }),
101
+ fcParams(5)
102
+ );
103
+ });
104
+
105
+ // Property: arbitrary mixed-case metadata survives the create → get round-trip.
106
+ test("metadata round-trips through create and get unchanged", async () => {
107
+ await fc.assert(
108
+ fc.asyncProperty(metadata(), async (meta) => {
109
+ const created = await authorizeViaCheckoutSession(gr4vy, 1299, {
110
+ metadata: meta,
111
+ });
112
+ const fetched = await gr4vy.transactions.get(created.id);
113
+ expect(fetched.metadata).toEqual(meta);
114
+ }),
115
+ fcParams(4)
116
+ );
117
+ });
118
+ });
@@ -0,0 +1,87 @@
1
+ import fc from "fast-check";
2
+ import { Address, BillingDetails, CartItem } from "../../src/models/components";
3
+
4
+ /**
5
+ * Shared fast-check parameters for E2E property tests. `numRuns` is deliberately
6
+ * small because every run is a real network round-trip — we want broad-ish input
7
+ * coverage without blowing the CI time budget. The seed is fixed (overridable via
8
+ * `FC_SEED`) so a failing case is reproducible from the CI logs.
9
+ */
10
+ const DEFAULT_SEED = 42;
11
+
12
+ // Use FC_SEED when it parses to a real number; otherwise fall back to the
13
+ // default so a non-numeric env value (common in CI templating) can't turn the
14
+ // seed into NaN and make fast-check behave unexpectedly.
15
+ const resolveSeed = (): number => {
16
+ const raw = process.env["FC_SEED"];
17
+ if (!raw) return DEFAULT_SEED;
18
+ const parsed = Number(raw);
19
+ return Number.isFinite(parsed) ? parsed : DEFAULT_SEED;
20
+ };
21
+
22
+ export const fcParams = <Ts = unknown>(numRuns = 5): fc.Parameters<Ts> => ({
23
+ numRuns,
24
+ seed: resolveSeed(),
25
+ });
26
+
27
+ /** Minor-unit amount the mock connector reliably authorises. */
28
+ export const amount = (): fc.Arbitrary<number> =>
29
+ fc.integer({ min: 50, max: 100_000 });
30
+
31
+ /** Currencies the test merchant's `mock-card` service accepts. */
32
+ export const currency = (): fc.Arbitrary<string> => fc.constantFrom("USD");
33
+
34
+ /** Countries the test merchant's `mock-card` service accepts. */
35
+ export const country = (): fc.Arbitrary<string> => fc.constantFrom("US");
36
+
37
+ /**
38
+ * Metadata maps mixing camelCase and snake_case keys, to exercise the
39
+ * snakecase-keys serialisation boundary the way the auth/embed tests do. Values
40
+ * are always strings to match the transaction metadata type.
41
+ */
42
+ export const metadata = (): fc.Arbitrary<Record<string, string>> =>
43
+ fc.dictionary(
44
+ fc.oneof(
45
+ fc.constantFrom("orderId", "campaign", "noteForOps"),
46
+ fc.constantFrom("order_id", "campaign_ref", "internal_note")
47
+ ),
48
+ fc.string({ minLength: 1, maxLength: 40 }),
49
+ { minKeys: 1, maxKeys: 4 }
50
+ );
51
+
52
+ /** Human-ish names with the odd unicode/whitespace edge case. */
53
+ export const name = (): fc.Arbitrary<string> =>
54
+ fc
55
+ .string({ minLength: 1, maxLength: 30 })
56
+ .filter((s) => s.trim().length > 0);
57
+
58
+ export const addressArb = (): fc.Arbitrary<Address> =>
59
+ fc.record(
60
+ {
61
+ city: fc.constantFrom("London", "Paris", "Berlin", "San Francisco"),
62
+ country: country(),
63
+ postalCode: fc.constantFrom("94110", "10001", "SW1A 1AA"),
64
+ state: fc.constantFrom("CA", "NY", "TX"),
65
+ line1: fc.string({ minLength: 1, maxLength: 50 }),
66
+ line2: fc.option(fc.string({ maxLength: 20 }), { nil: undefined }),
67
+ },
68
+ { requiredKeys: ["country", "line1"] }
69
+ );
70
+
71
+ export const billingDetailsArb = (): fc.Arbitrary<BillingDetails> =>
72
+ fc.record(
73
+ {
74
+ firstName: name(),
75
+ lastName: name(),
76
+ emailAddress: fc.emailAddress(),
77
+ address: addressArb(),
78
+ },
79
+ { requiredKeys: ["firstName", "lastName"] }
80
+ );
81
+
82
+ export const cartItemArb = (): fc.Arbitrary<CartItem> =>
83
+ fc.record({
84
+ name: name(),
85
+ quantity: fc.integer({ min: 1, max: 5 }),
86
+ unitAmount: fc.integer({ min: 1, max: 5_000 }),
87
+ });