@gr4vy/sdk 2.1.2 → 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.
package/README.md CHANGED
@@ -1090,13 +1090,19 @@ You can also enable a default debug logger by setting an environment variable `G
1090
1090
 
1091
1091
  ## Testing
1092
1092
 
1093
- To run the tests, install NPM, ensure to download the `private_key.pem` for the test environment, and run the following.
1093
+ The SDK has an end-to-end test suite that runs against the E2E sandbox. Place
1094
+ the `private_key.pem` for the test environment in the repo root (or export
1095
+ `PRIVATE_KEY`), then:
1094
1096
 
1095
1097
  ```sh
1096
1098
  npm install
1097
- npx vitest --testTimeout 8000
1099
+ npx vitest run
1098
1100
  ```
1099
1101
 
1102
+ See **[TESTING.md](TESTING.md)** for the full guide — suite layout, parallelism
1103
+ and CI sharding, property-based tests, coverage / endpoint-reach reporting, and
1104
+ how testing ties into the release pipeline.
1105
+
1100
1106
  ## Contributions
1101
1107
 
1102
1108
  While we value open-source contributions to this SDK, this library is generated programmatically.
package/jsr.json CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  {
4
4
  "name": "@gr4vy/sdk",
5
- "version": "2.1.2",
5
+ "version": "2.1.3",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
8
8
  "./models/errors": "./src/models/errors/index.ts",
package/lib/config.d.ts CHANGED
@@ -44,8 +44,8 @@ export declare function serverURLFromOptions(options: SDKOptions): URL | null;
44
44
  export declare const SDK_METADATA: {
45
45
  readonly language: "typescript";
46
46
  readonly openapiDocVersion: "1.0.0";
47
- readonly sdkVersion: "2.1.2";
47
+ readonly sdkVersion: "2.1.3";
48
48
  readonly genVersion: "2.889.1";
49
- readonly userAgent: "speakeasy-sdk/typescript 2.1.2 2.889.1 1.0.0 @gr4vy/sdk";
49
+ readonly userAgent: "speakeasy-sdk/typescript 2.1.3 2.889.1 1.0.0 @gr4vy/sdk";
50
50
  };
51
51
  //# sourceMappingURL=config.d.ts.map
package/lib/config.js CHANGED
@@ -37,8 +37,8 @@ function serverURLFromOptions(options) {
37
37
  exports.SDK_METADATA = {
38
38
  language: "typescript",
39
39
  openapiDocVersion: "1.0.0",
40
- sdkVersion: "2.1.2",
40
+ sdkVersion: "2.1.3",
41
41
  genVersion: "2.889.1",
42
- userAgent: "speakeasy-sdk/typescript 2.1.2 2.889.1 1.0.0 @gr4vy/sdk",
42
+ userAgent: "speakeasy-sdk/typescript 2.1.3 2.889.1 1.0.0 @gr4vy/sdk",
43
43
  };
44
44
  //# sourceMappingURL=config.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gr4vy/sdk",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "author": "Gr4vy",
5
5
  "main": "./index.js",
6
6
  "sideEffects": false,
@@ -19,12 +19,14 @@
19
19
  "devDependencies": {
20
20
  "@eslint/js": "^9.26.0",
21
21
  "@types/jsonwebtoken": "^9.0.10",
22
+ "@vitest/coverage-v8": "^4.1.7",
22
23
  "eslint": "^9.26.0",
24
+ "fast-check": "^4.8.0",
23
25
  "globals": "^15.14.0",
24
26
  "timekeeper": "^2.3.1",
25
27
  "typescript": "~5.8.3",
26
28
  "typescript-eslint": "^8.26.0",
27
- "vitest": "^4.1.5"
29
+ "vitest": "^4.1.7"
28
30
  },
29
31
  "dependencies": {
30
32
  "jsonwebtoken": "^9.0.3",
@@ -0,0 +1,136 @@
1
+ // Summarises test coverage for the PR comment.
2
+ //
3
+ // Endpoint reach is computed from *observed HTTP calls*, not statement coverage:
4
+ // the test fetcher (tests/utils/setup.ts, gated on GR4VY_TRACK_HTTP) logs the
5
+ // method + path of every request it sends to coverage/http/*.jsonl. We map each
6
+ // `src/funcs/*.ts` operation to its (method, path-template) and mark it reached
7
+ // only if a matching request was actually sent — so a function that returns at
8
+ // local validation before issuing a request does not count.
9
+ //
10
+ // Code-coverage percentages (for the instrumented SDK surface) are read from the
11
+ // vitest v8 coverage-summary.json as a secondary signal.
12
+ //
13
+ // Writes coverage/endpoint-coverage.md (for the PR comment) and prints it.
14
+ // Exits 0 always — this is a report, not a gate.
15
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+
18
+ const COVERAGE_DIR = "coverage";
19
+ const FUNCS_DIR = "src/funcs";
20
+
21
+ // 1. Build the operation catalogue from the generated funcs.
22
+ const opCatalogue = readdirSync(FUNCS_DIR)
23
+ .filter((f) => f.endsWith(".ts"))
24
+ .map((file) => {
25
+ const src = readFileSync(join(FUNCS_DIR, file), "utf8");
26
+ const pathMatch = src.match(/pathToFunc\(\s*"([^"]+)"/);
27
+ const methodMatch = src.match(/method:\s*"(GET|POST|PUT|PATCH|DELETE)"/);
28
+ if (!pathMatch || !methodMatch) return null;
29
+ const template = pathMatch[1];
30
+ const regex = new RegExp(
31
+ "^" + template.replace(/\{[^/}]+\}/g, "[^/]+").replace(/\//g, "\\/") + "$"
32
+ );
33
+ const params = (template.match(/\{[^/}]+\}/g) ?? []).length;
34
+ return { op: file.replace(/\.ts$/, ""), method: methodMatch[1], template, regex, params };
35
+ })
36
+ .filter(Boolean);
37
+
38
+ // 2. Load observed HTTP calls (per-worker files).
39
+ let calls = [];
40
+ try {
41
+ const dir = join(COVERAGE_DIR, "http");
42
+ for (const f of readdirSync(dir).filter((f) => f.endsWith(".jsonl"))) {
43
+ for (const line of readFileSync(join(dir, f), "utf8").split("\n")) {
44
+ if (!line.trim()) continue;
45
+ // Skip malformed/truncated lines (e.g. a worker that crashed mid-write) —
46
+ // this is best-effort instrumentation and must never fail the report.
47
+ try {
48
+ calls.push(JSON.parse(line));
49
+ } catch {
50
+ // ignore this line
51
+ }
52
+ }
53
+ }
54
+ } catch {
55
+ // no logs — fetcher tracking was not enabled for this run
56
+ }
57
+
58
+ const httpTracked = calls.length > 0;
59
+
60
+ // 3. Match calls to operations.
61
+ const reached = new Set();
62
+ if (httpTracked) {
63
+ for (const { method, pathname } of calls) {
64
+ // A literal path like /buyers/gift-cards also matches a parameterised
65
+ // template like /buyers/{buyer_id}, so credit the most specific operation
66
+ // (fewest path params, then longest template).
67
+ const op = opCatalogue
68
+ .filter((o) => o.method === method && o.regex.test(pathname))
69
+ .sort((a, b) => a.params - b.params || b.template.length - a.template.length)[0];
70
+ if (op) reached.add(op.op);
71
+ }
72
+ }
73
+
74
+ const missed = opCatalogue
75
+ .map((o) => o.op)
76
+ .filter((op) => !reached.has(op))
77
+ .sort();
78
+
79
+ // 4. Code-coverage percentages (secondary signal).
80
+ let total = {};
81
+ try {
82
+ total =
83
+ JSON.parse(readFileSync(join(COVERAGE_DIR, "coverage-summary.json"), "utf8"))
84
+ .total ?? {};
85
+ } catch {
86
+ // coverage summary missing — leave percentages blank
87
+ }
88
+ const pct = (m) => (total[m]?.pct != null ? `${total[m].pct.toFixed(1)}%` : "n/a");
89
+
90
+ // 5. Render.
91
+ const lines = [];
92
+ lines.push("### 🧪 Test coverage");
93
+ lines.push("");
94
+ if (!httpTracked) {
95
+ lines.push(
96
+ "> ⚠️ HTTP call tracking was not enabled (set `GR4VY_TRACK_HTTP=1` for the " +
97
+ "coverage run), so endpoint reach could not be computed from observed requests."
98
+ );
99
+ lines.push("");
100
+ }
101
+ const reachPct = opCatalogue.length
102
+ ? ((reached.size / opCatalogue.length) * 100).toFixed(1)
103
+ : "0.0";
104
+ lines.push("| Metric | Value |");
105
+ lines.push("| --- | --- |");
106
+ lines.push(
107
+ `| **Endpoints reached (HTTP)** | ${reached.size} / ${opCatalogue.length} (${reachPct}%) |`
108
+ );
109
+ lines.push(`| Statements | ${pct("statements")} |`);
110
+ lines.push(`| Branches | ${pct("branches")} |`);
111
+ lines.push(`| Functions | ${pct("functions")} |`);
112
+ lines.push(`| Lines | ${pct("lines")} |`);
113
+ lines.push("");
114
+ if (httpTracked && missed.length) {
115
+ lines.push(
116
+ `> ⚠️ **${missed.length} endpoint operation(s) have no E2E test.** ` +
117
+ `Newly generated endpoints show up here — consider adding tests for them.`
118
+ );
119
+ lines.push("");
120
+ for (const name of missed) lines.push(`- \`${name}\``);
121
+ } else if (httpTracked) {
122
+ lines.push("✅ Every endpoint operation was reached by a real request.");
123
+ }
124
+ lines.push("");
125
+ lines.push(
126
+ "<sub>Endpoint reach is measured from HTTP requests actually sent by the " +
127
+ "suite (see tests/utils/setup.ts). Code coverage is for `src/funcs` + " +
128
+ "`src/sdk`. See [TESTING.md](../TESTING.md).</sub>"
129
+ );
130
+
131
+ const markdown = lines.join("\n");
132
+ // Ensure the output dir exists — the script may run even when --coverage wasn't,
133
+ // and it must never fail the job just because of a missing directory.
134
+ mkdirSync(COVERAGE_DIR, { recursive: true });
135
+ writeFileSync(join(COVERAGE_DIR, "endpoint-coverage.md"), markdown + "\n");
136
+ console.log(markdown);
package/src/lib/config.ts CHANGED
@@ -77,7 +77,7 @@ export function serverURLFromOptions(options: SDKOptions): URL | null {
77
77
  export const SDK_METADATA = {
78
78
  language: "typescript",
79
79
  openapiDocVersion: "1.0.0",
80
- sdkVersion: "2.1.2",
80
+ sdkVersion: "2.1.3",
81
81
  genVersion: "2.889.1",
82
- userAgent: "speakeasy-sdk/typescript 2.1.2 2.889.1 1.0.0 @gr4vy/sdk",
82
+ userAgent: "speakeasy-sdk/typescript 2.1.3 2.889.1 1.0.0 @gr4vy/sdk",
83
83
  } as const;
@@ -0,0 +1,24 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { 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
+ describe("Account Updater", () => {
13
+ // The Real-Time Account Updater (Loon) is not configured on the mock merchant,
14
+ // so creating a job is exercised at the request level: a real stored payment
15
+ // method id is submitted and the API is expected to reject the job because the
16
+ // service is not enabled, rather than the test skipping the endpoint.
17
+ test("creating a job is exercised at the request level", async () => {
18
+ const method = await gr4vy.paymentMethods.create(cardPaymentMethod());
19
+
20
+ await expect(
21
+ gr4vy.accountUpdater.jobs.create({ paymentMethodIds: [method.id] })
22
+ ).rejects.toThrow();
23
+ });
24
+ });
@@ -0,0 +1,20 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { buyer } 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("Audit Logs", () => {
13
+ test("listing audit logs returns a page", async () => {
14
+ // Perform a mutating action so the account has at least one audit entry.
15
+ await gr4vy.buyers.create(buyer());
16
+
17
+ const page = await gr4vy.auditLogs.list({});
18
+ expect(page).toBeDefined();
19
+ });
20
+ });
@@ -0,0 +1,77 @@
1
+ import crypto from "crypto";
2
+ import { beforeAll, describe, expect, test } from "vitest";
3
+ import { Gr4vy } from "../../src";
4
+ import { createGr4vyClient, setupMerchant } from "../utils/setup";
5
+
6
+ let admin: Gr4vy;
7
+ let privateKey: string;
8
+
9
+ beforeAll(async () => {
10
+ // merchant-accounts are an admin-level resource, so use an un-scoped client.
11
+ ({ privateKey } = await setupMerchant());
12
+ admin = createGr4vyClient(privateKey);
13
+ });
14
+
15
+ const newMerchantId = () => crypto.randomBytes(8).toString("hex");
16
+
17
+ describe("Merchant Accounts", () => {
18
+ test("create → get → list → update (partial)", async () => {
19
+ const id = newMerchantId();
20
+ const created = await admin.merchantAccounts.create({
21
+ id,
22
+ displayName: "Original Display",
23
+ });
24
+ expect(created.id).toBe(id);
25
+
26
+ const fetched = await admin.merchantAccounts.get(id);
27
+ expect(fetched.id).toBe(id);
28
+
29
+ const page = await admin.merchantAccounts.list();
30
+ expect(page).toBeDefined();
31
+
32
+ // Partial update: change only the display name, leave everything else.
33
+ const updated = await admin.merchantAccounts.update(
34
+ { displayName: "Renamed Display" },
35
+ id
36
+ );
37
+ expect(updated.displayName).toBe("Renamed Display");
38
+ expect(updated.id).toBe(id);
39
+ });
40
+
41
+ describe("3DS configuration", () => {
42
+ // 3DS configuration is keyed per scheme/currency on the merchant account.
43
+ test("create → list → update (partial) → delete", async () => {
44
+ const id = newMerchantId();
45
+ await admin.merchantAccounts.create({ id, displayName: id });
46
+
47
+ const created =
48
+ await admin.merchantAccounts.threeDsConfiguration.create(
49
+ {
50
+ merchantAcquirerBin: "516327",
51
+ merchantAcquirerId: "123456789012345",
52
+ merchantName: "Gr4vy E2E",
53
+ merchantCountryCode: "840",
54
+ merchantCategoryCode: "1234",
55
+ merchantUrl: "https://example.com",
56
+ scheme: "visa",
57
+ metadata: { env: "e2e" },
58
+ },
59
+ id
60
+ );
61
+ expect(created.id).toBeDefined();
62
+
63
+ const list = await admin.merchantAccounts.threeDsConfiguration.list(id);
64
+ expect(list).toBeDefined();
65
+
66
+ const updated =
67
+ await admin.merchantAccounts.threeDsConfiguration.update(
68
+ { merchantName: "Renamed Merchant" },
69
+ id,
70
+ created.id!
71
+ );
72
+ expect(updated.merchantName).toBe("Renamed Merchant");
73
+
74
+ await admin.merchantAccounts.threeDsConfiguration.delete(id, created.id!);
75
+ });
76
+ });
77
+ });
@@ -0,0 +1,95 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { PaymentServiceCreate } from "../../src/models/components";
4
+ import { setupMerchant } from "../utils/setup";
5
+
6
+ let gr4vy: Gr4vy;
7
+
8
+ beforeAll(async () => {
9
+ ({ client: gr4vy } = await setupMerchant());
10
+ });
11
+
12
+ const mockCardService = (
13
+ overrides: Partial<PaymentServiceCreate> = {}
14
+ ): PaymentServiceCreate => ({
15
+ displayName: "Mock card service",
16
+ paymentServiceDefinitionId: "mock-card",
17
+ acceptedCurrencies: ["USD"],
18
+ acceptedCountries: ["US"],
19
+ fields: [{ key: "merchant_id", value: "test" }],
20
+ ...overrides,
21
+ });
22
+
23
+ describe("Payment Services", () => {
24
+ test("create → get → list → update (partial) → delete", async () => {
25
+ const created = await gr4vy.paymentServices.create(mockCardService());
26
+ expect(created.id).toBeDefined();
27
+
28
+ const fetched = await gr4vy.paymentServices.get(created.id!);
29
+ expect(fetched.id).toBe(created.id);
30
+
31
+ const page = await gr4vy.paymentServices.list({});
32
+ expect(page).toBeDefined();
33
+
34
+ // Partial update: change only the display name.
35
+ const updated = await gr4vy.paymentServices.update(
36
+ { displayName: "Renamed service" },
37
+ created.id!
38
+ );
39
+ expect(updated.displayName).toBe("Renamed service");
40
+
41
+ await gr4vy.paymentServices.delete(created.id!);
42
+ await expect(() => gr4vy.paymentServices.get(created.id!)).rejects.toThrow();
43
+ });
44
+
45
+ // Credential verification and session creation are not meaningfully supported
46
+ // by the mock-card connector (verify returns a non-JSON body; session is
47
+ // explicitly unsupported), so both are exercised at the request level.
48
+ test("verify is exercised at the request level", async () => {
49
+ await expect(
50
+ gr4vy.paymentServices.verify({
51
+ paymentServiceDefinitionId: "mock-card",
52
+ fields: [{ key: "merchant_id", value: "test" }],
53
+ })
54
+ ).rejects.toThrow();
55
+ });
56
+
57
+ test("create a session is exercised at the request level", async () => {
58
+ const created = await gr4vy.paymentServices.create(mockCardService());
59
+ await expect(
60
+ gr4vy.paymentServices.session({}, created.id!)
61
+ ).rejects.toThrow();
62
+ });
63
+
64
+ describe("definitions, options and card schemes", () => {
65
+ test("list and get payment service definitions", async () => {
66
+ const page = await gr4vy.paymentServiceDefinitions.list();
67
+ expect(page).toBeDefined();
68
+
69
+ const definition =
70
+ await gr4vy.paymentServiceDefinitions.get("mock-card");
71
+ expect(definition.id).toBe("mock-card");
72
+ });
73
+
74
+ test("create a session for a definition is exercised at the request level", async () => {
75
+ // Session creation is not supported for the mock-card definition.
76
+ await expect(
77
+ gr4vy.paymentServiceDefinitions.session({}, "mock-card")
78
+ ).rejects.toThrow();
79
+ });
80
+
81
+ test("list payment options for a cart", async () => {
82
+ const options = await gr4vy.paymentOptions.list({
83
+ country: "US",
84
+ currency: "USD",
85
+ amount: 1299,
86
+ });
87
+ expect(options).toBeDefined();
88
+ });
89
+
90
+ test("list card scheme definitions", async () => {
91
+ const schemes = await gr4vy.cardSchemeDefinitions.list();
92
+ expect(schemes).toBeDefined();
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,49 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { APPROVING_CARD } 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("Payouts", () => {
13
+ test("listing payouts returns a page", async () => {
14
+ const page = await gr4vy.payouts.list();
15
+ expect(page).toBeDefined();
16
+ });
17
+
18
+ // The mock-card service is not a payout-capable connection, so creating a
19
+ // payout is exercised at the request level — the SDK serialises the payload
20
+ // and the API rejects it for a real reason rather than the test skipping it.
21
+ test("create is exercised at the request level", async () => {
22
+ const service = await gr4vy.paymentServices.create({
23
+ displayName: "Mock card service",
24
+ paymentServiceDefinitionId: "mock-card",
25
+ acceptedCurrencies: ["USD"],
26
+ acceptedCountries: ["US"],
27
+ fields: [{ key: "merchant_id", value: "test" }],
28
+ });
29
+
30
+ await expect(
31
+ gr4vy.payouts.create({
32
+ amount: 1299,
33
+ currency: "USD",
34
+ paymentServiceId: service.id,
35
+ paymentMethod: {
36
+ method: "card",
37
+ number: APPROVING_CARD.number,
38
+ expirationDate: APPROVING_CARD.expiration_date,
39
+ },
40
+ })
41
+ ).rejects.toThrow();
42
+ });
43
+
44
+ test("fetching a missing payout is exercised at the request level", async () => {
45
+ await expect(
46
+ gr4vy.payouts.get("00000000-0000-0000-0000-000000000000")
47
+ ).rejects.toThrow();
48
+ });
49
+ });
@@ -0,0 +1,69 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { ReportCreate } 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 reportCreate = (): ReportCreate => ({
14
+ name: uniqueId("report"),
15
+ schedule: "daily",
16
+ scheduleEnabled: true,
17
+ scheduleTimezone: "UTC",
18
+ spec: {
19
+ model: "transactions",
20
+ params: {
21
+ fields: ["id", "status"],
22
+ filters: { status: ["capture_succeeded"] },
23
+ sort: [{ field: "created_at", order: "desc" }],
24
+ },
25
+ },
26
+ });
27
+
28
+ describe("Reports", () => {
29
+ test("create → get → put (partial) → list", async () => {
30
+ const created = await gr4vy.reports.create(reportCreate());
31
+ expect(created.id).toBeDefined();
32
+
33
+ const fetched = await gr4vy.reports.get(created.id);
34
+ expect(fetched.id).toBe(created.id);
35
+
36
+ // Partial update: toggle the schedule without resending name/spec.
37
+ const updated = await gr4vy.reports.put(
38
+ { scheduleEnabled: false },
39
+ created.id
40
+ );
41
+ expect(updated.scheduleEnabled).toBe(false);
42
+
43
+ const page = await gr4vy.reports.list({});
44
+ expect(page).toBeDefined();
45
+ });
46
+
47
+ test("report executions are listable per-report and account-wide", async () => {
48
+ const created = await gr4vy.reports.create(reportCreate());
49
+
50
+ const perReport = await gr4vy.reports.executions.list(created.id);
51
+ expect(perReport).toBeDefined();
52
+
53
+ const accountWide = await gr4vy.reportExecutions.list({});
54
+ expect(accountWide).toBeDefined();
55
+ });
56
+
57
+ // A freshly created report has no executions yet, so fetching one / generating
58
+ // a download URL is exercised at the request level against a non-existent
59
+ // execution id; the API is expected to reject it.
60
+ test("fetching a missing execution / url is exercised at the request level", async () => {
61
+ const created = await gr4vy.reports.create(reportCreate());
62
+ const bogus = "00000000-0000-0000-0000-000000000000";
63
+
64
+ await expect(gr4vy.reports.executions.get(bogus)).rejects.toThrow();
65
+ await expect(
66
+ gr4vy.reports.executions.url(created.id, bogus)
67
+ ).rejects.toThrow();
68
+ });
69
+ });
@@ -0,0 +1,34 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import { ThreeDSecureScenarioCreate } from "../../src/models/components";
4
+ import { setupMerchant } from "../utils/setup";
5
+
6
+ let gr4vy: Gr4vy;
7
+
8
+ beforeAll(async () => {
9
+ ({ client: gr4vy } = await setupMerchant());
10
+ });
11
+
12
+ const scenario = (): ThreeDSecureScenarioCreate => ({
13
+ conditions: { amount: 5000 },
14
+ outcome: { authentication: { transactionStatus: "Y" } },
15
+ });
16
+
17
+ describe("3DS Scenarios", () => {
18
+ test("create → list → update (partial) → delete", async () => {
19
+ const created = await gr4vy.threeDsScenarios.create(scenario());
20
+ expect(created.id).toBeDefined();
21
+
22
+ const page = await gr4vy.threeDsScenarios.list();
23
+ expect(page).toBeDefined();
24
+
25
+ // Partial update: replace only the outcome, leaving the conditions intact.
26
+ const updated = await gr4vy.threeDsScenarios.update(
27
+ { outcome: { authentication: { transactionStatus: "N" } } },
28
+ created.id
29
+ );
30
+ expect(updated.id).toBe(created.id);
31
+
32
+ await gr4vy.threeDsScenarios.delete(created.id);
33
+ });
34
+ });
@@ -0,0 +1,74 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { Gr4vy } from "../../src";
3
+ import {
4
+ address,
5
+ billingDetails,
6
+ buyer,
7
+ cardPaymentMethod,
8
+ uniqueId,
9
+ } from "../utils/fixtures";
10
+ import { setupMerchant } from "../utils/setup";
11
+
12
+ let gr4vy: Gr4vy;
13
+
14
+ beforeAll(async () => {
15
+ ({ client: gr4vy } = await setupMerchant());
16
+ });
17
+
18
+ describe("Buyer lifecycle", () => {
19
+ test("create buyer, store a card against them, transact, and read it back", async () => {
20
+ // 1. Create a buyer.
21
+ const created = await gr4vy.buyers.create(buyer({ displayName: "Ada Lovelace" }));
22
+ expect(created.id).toBeDefined();
23
+ expect(created.displayName).toBe("Ada Lovelace");
24
+
25
+ // 2. Store a card payment method against the buyer.
26
+ const method = await gr4vy.paymentMethods.create({
27
+ ...cardPaymentMethod(),
28
+ buyerId: created.id,
29
+ });
30
+ expect(method.id).toBeDefined();
31
+
32
+ // 3. The stored method shows up under the buyer's payment methods.
33
+ const buyerMethods = await gr4vy.buyers.paymentMethods.list({
34
+ buyerId: created.id,
35
+ });
36
+ expect(buyerMethods.items.some((m) => m.id === method.id)).toBe(true);
37
+
38
+ // 4. Add shipping details for the buyer.
39
+ const shipping = await gr4vy.buyers.shippingDetails.create(
40
+ {
41
+ firstName: "Ada",
42
+ lastName: "Lovelace",
43
+ address: address(),
44
+ },
45
+ created.id!
46
+ );
47
+ expect(shipping.id).toBeDefined();
48
+
49
+ // 5. Charge the stored method, referencing the buyer + shipping details.
50
+ const transaction = await gr4vy.transactions.create({
51
+ amount: 1299,
52
+ currency: "USD",
53
+ buyerId: created.id,
54
+ shippingDetailsId: shipping.id,
55
+ externalIdentifier: uniqueId("txn"),
56
+ paymentMethod: { method: "id", id: method.id, securityCode: "123" },
57
+ });
58
+ expect(transaction.status).toBe("authorization_succeeded");
59
+
60
+ // 6. Gift cards list for the buyer is reachable (empty is fine).
61
+ const giftCards = await gr4vy.buyers.giftCards.list({ buyerId: created.id });
62
+ expect(Array.isArray(giftCards.items)).toBe(true);
63
+ });
64
+
65
+ test("buyer can be created with billing details and deleted", async () => {
66
+ const created = await gr4vy.buyers.create(
67
+ buyer({ billingDetails: billingDetails({ firstName: "Grace" }) })
68
+ );
69
+ expect(created.billingDetails?.firstName).toBe("Grace");
70
+
71
+ await gr4vy.buyers.delete(created.id!);
72
+ await expect(() => gr4vy.buyers.get(created.id!)).rejects.toThrow();
73
+ });
74
+ });