Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ A _curated_ set of hooks for the most common endpoints. Every hook is end-to-end
| `useRegenerateApiKeysMutation` | `api.apikeys.regenerateApiKeys` | invalidates `ApiKey/LIST` |
| `useGetPublicFileQuery` | `api.files.getPublicFile` | provides `Files/PUBLIC:<publicId>` |
| `useTestFilesIntegrationApiMutation` | `api.files.testFilesIntegration` | invalidates `Files` + `FilesIntegrations` |
| `useCreatePaymentOrderMutation` | `api.payments.createPaymentOrder` | invalidates `PaymentOrders/LIST` |
| `useGetPaymentOrderQuery` | `api.payments.getPaymentOrder` | provides `PaymentOrders/<id>` |
| `useGetPaymentOrdersQuery` | `api.payments.getPaymentOrders` | provides `PaymentOrders/LIST` |
| `useStartPaymentCheckoutMutation` | `api.payments.startPaymentCheckout` | invalidates `PaymentOrders/LIST` + `PaymentOrders/<id>` |

### Hub surface — `norbix.hub.*`

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"@norbix.ai/ts": ">=4.26.0",
"@norbix.ai/ts": ">=4.29.0",
"@reduxjs/toolkit": ">=2.0.0",
"react": ">=18.0.0",
"react-redux": ">=9.0.0"
Expand All @@ -68,7 +68,7 @@
"@commitlint/cli": "^21.2.2",
"@commitlint/config-conventional": "^21.2.2",
"@eslint/js": "^10.0.1",
"@norbix.ai/ts": "^4.26.0",
"@norbix.ai/ts": "^4.29.0",
"@reduxjs/toolkit": "^2.12.0",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/github": "^12.0.9",
Expand Down
1 change: 1 addition & 0 deletions src/createNorbixApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function createNorbixApi(
'PaymentPlans',
'PaymentDiscounts',
'PaymentCustomers',
'PaymentOrders',
// AI
'Ai',
'AiChat',
Expand Down
77 changes: 77 additions & 0 deletions src/hooks/api/payments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// `norbix.api.payments` — payment orders and their checkout (4 endpoints).
//
// The gateway routes behind these hooks, so the endpoint-coverage matrix can
// see them (it looks for the route string in the source). This file never
// builds a URL itself — the core SDK does — so these are documentation:
//
// POST /{version}/payments/orders
// GET /{version}/payments/orders
// GET /{version}/payments/orders/{Id}
// POST /{version}/payments/orders/{Id}/checkout
import type { Norbix } from '@norbix.ai/ts';

import type { Builder } from '../index.js';
import type { Arg, Result } from '../types.js';

type CreatePaymentOrder = Norbix['api']['payments']['createPaymentOrder'];
type GetPaymentOrder = Norbix['api']['payments']['getPaymentOrder'];
type GetPaymentOrders = Norbix['api']['payments']['getPaymentOrders'];
type StartPaymentCheckout = Norbix['api']['payments']['startPaymentCheckout'];

/**
* `api.payments` — 4 endpoints, 1:1 with the core SDK.
*
* Cache: every order list is `PaymentOrders/LIST`, one order is
* `PaymentOrders/<id>`. Creating an order invalidates the lists; starting a
* checkout invalidates the lists and that order (a checkout attempt is added
* to it, and the gateway may move its status).
*
* An order's status also moves when the provider confirms the payment, which
* happens outside this store — refetch (or poll) `useGetPaymentOrderQuery`
* after the buyer comes back on the `returnUrl`.
*/
export const apiPayments = (b: Builder) => ({
/**
* Creates a payment order: `buyerUserId`, `merchantReference`, `currency`,
* `priceVersion`, `lines` (`sku?`, `title?`, `quantity`, `unitAmountMinor`
* in minor units) and an optional `metadata` string map. Answers `{ id }`
* (`ord_…`). Send an `idempotencyKey` (8+ characters) to make a retry
* safe: the same body with the same key answers the same id.
*/
createPaymentOrder: b.mutation<Result<CreatePaymentOrder>, Arg<CreatePaymentOrder>>({
query: (args) => (norbix) => norbix.api.payments.createPaymentOrder(args),
invalidatesTags: [{ type: 'PaymentOrders', id: 'LIST' }],
}),

/** Reads one payment order (`id`). Answers `{ item: PaymentOrderDto }`. */
getPaymentOrder: b.query<Result<GetPaymentOrder>, Arg<GetPaymentOrder>>({
query: (args) => (norbix) => norbix.api.payments.getPaymentOrder(args),
providesTags: (_res, _err, arg) => [
{ type: 'PaymentOrders' as const, id: arg?.id ?? 'unknown' },
],
}),

/**
* Lists payment orders, filtered by the optional `buyerUserId`,
* `merchantReference` and `status`. Answers `{ list }` (paginated
* `PaymentOrderDto`).
*/
getPaymentOrders: b.query<Result<GetPaymentOrders>, Arg<GetPaymentOrders>>({
query: (args) => (norbix) => norbix.api.payments.getPaymentOrders(args),
providesTags: [{ type: 'PaymentOrders', id: 'LIST' }],
}),

/**
* Starts (or reuses) a checkout for an order: `id`, `integrationId`,
* `returnUrl`, and optionally `method`, `preferredCountry`,
* `preferredProvider`. Answers `{ checkout, reused, order }` — send the
* buyer to `checkout.paymentUrl`.
*/
startPaymentCheckout: b.mutation<Result<StartPaymentCheckout>, Arg<StartPaymentCheckout>>({
query: (args) => (norbix) => norbix.api.payments.startPaymentCheckout(args),
invalidatesTags: (_res, _err, arg) => [
{ type: 'PaymentOrders' as const, id: 'LIST' },
{ type: 'PaymentOrders' as const, id: arg?.id ?? 'unknown' },
],
}),
});
3 changes: 3 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { apiAuth } from './api/auth.js';
import { apiDatabase } from './api/database.js';
import { apiFiles } from './api/files.js';
import { apiMembership } from './api/membership.js';
import { apiPayments } from './api/payments.js';
import { apiPublic } from './api/public.js';
import { apiPush } from './api/push.js';
import { hubAccount } from './hub/account.js';
Expand Down Expand Up @@ -103,6 +104,7 @@ export type AllTags =
| 'PaymentPlans'
| 'PaymentDiscounts'
| 'PaymentCustomers'
| 'PaymentOrders'
// ---- AI ----
| 'Ai'
| 'AiChat'
Expand Down Expand Up @@ -143,6 +145,7 @@ export function buildEndpoints(builder: Builder) {
...apiDatabase(builder),
...apiApikeys(builder),
...apiFiles(builder),
...apiPayments(builder),
...apiPublic(builder),
...apiPush(builder),
// Hub surface (control plane)
Expand Down
99 changes: 98 additions & 1 deletion tests/payments.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest';

import { apiPayments } from '../src/hooks/api/payments.js';
import { hubPayments } from '../src/hooks/hub/payments.js';
import { buildEndpoints } from '../src/hooks/index.js';

/**
* `hub.payments` hooks (gateway campaign audit/payments, item 8). Same fake
Expand All @@ -15,6 +17,8 @@ function fakeBuilder() {
} as never;
}

type TagFn = (res: unknown, err: unknown, arg: unknown) => unknown;

type Def = {
__kind: 'query' | 'mutation';
query: (args: unknown) => (norbix: unknown) => unknown;
Expand All @@ -33,7 +37,16 @@ function fakeNorbix() {
},
},
);
return { calls, client: { hub: { payments } } };
const apiPaymentsClient = new Proxy(
{},
{
get: (_t, name: string) => (args: unknown) => {
calls.push({ method: `api.${name}`, args });
return Promise.resolve({ ok: true });
},
},
);
return { calls, client: { hub: { payments }, api: { payments: apiPaymentsClient } } };
}

describe('hubPayments', () => {
Expand All @@ -55,3 +68,87 @@ describe('hubPayments', () => {
expect(calls).toEqual([{ method: 'hub.getPaymentsWebhookLog', args }]);
});
});

/**
* `api.payments` hooks — payment orders + checkout (campaign
* audit/payments-checkout, item sdk-all; core SDK 4.29.0).
*/
describe('apiPayments', () => {
const endpoints = apiPayments(fakeBuilder()) as unknown as Record<string, Def>;

it('covers the 4 api Payments endpoints of the core SDK', () => {
expect(Object.keys(endpoints).sort()).toEqual(
['createPaymentOrder', 'getPaymentOrder', 'getPaymentOrders', 'startPaymentCheckout'].sort(),
);
});

it.each([
[
'createPaymentOrder',
'mutation',
{
buyerUserId: 'usr_1',
merchantReference: 'listing-42',
idempotencyKey: 'listing-42-key',
currency: 'EUR',
priceVersion: 'v1',
lines: [{ sku: 'promo', quantity: 21, unitAmountMinor: 30 }],
},
],
['getPaymentOrder', 'query', { id: 'ord_1' }],
['getPaymentOrders', 'query', { buyerUserId: 'usr_1', status: 'Paid' }],
[
'startPaymentCheckout',
'mutation',
{ id: 'ord_1', integrationId: 'int_montonio', returnUrl: 'https://shop.example/return' },
],
])('%s is a %s that reaches api.payments.%s with the caller args', async (key, kind, args) => {
const { calls, client } = fakeNorbix();

await endpoints[key].query(args)(client);

expect(endpoints[key].__kind).toBe(kind);
expect(calls).toEqual([{ method: `api.${key}`, args }]);
});

it('getPaymentOrders provides the order list', () => {
expect(endpoints.getPaymentOrders.providesTags).toEqual([
{ type: 'PaymentOrders', id: 'LIST' },
]);
});

it('getPaymentOrder provides the order by its id', () => {
const tags = (endpoints.getPaymentOrder.providesTags as TagFn)(undefined, undefined, {
id: 'ord_1',
});
expect(tags).toEqual([{ type: 'PaymentOrders', id: 'ord_1' }]);
});

it('createPaymentOrder invalidates the order lists', () => {
expect(endpoints.createPaymentOrder.invalidatesTags).toEqual([
{ type: 'PaymentOrders', id: 'LIST' },
]);
});

it('startPaymentCheckout invalidates the order lists and that order', () => {
const tags = (endpoints.startPaymentCheckout.invalidatesTags as TagFn)(undefined, undefined, {
id: 'ord_1',
integrationId: 'int_montonio',
});
expect(tags).toEqual([
{ type: 'PaymentOrders', id: 'LIST' },
{ type: 'PaymentOrders', id: 'ord_1' },
]);
});

it('is spread into the full endpoint set, and no Hub hook replaces it', async () => {
const all = buildEndpoints(fakeBuilder()) as unknown as Record<string, Def>;
const { calls, client } = fakeNorbix();

for (const key of Object.keys(endpoints)) {
await all[key].query({ id: 'ord_1' })(client);
}

expect(calls.map((c) => c.method)).toEqual(Object.keys(endpoints).map((k) => `api.${k}`));
});
});