Skip to content
Open
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
158 changes: 158 additions & 0 deletions packages/core/src/__tests__/payment-links-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { DfxHttpClient } from '../client/DfxHttpClient';
import { PaymentLinksApi } from '../client/PaymentLinksApi';
import {
PaymentLinkMode,
PaymentLinkPayResponse,
PaymentLinkPaymentStatus,
PaymentStandardType,
hasPaymentQuote,
} from '../definitions/route';

function createMockHttpClient(response?: any) {
const requestMock = jest.fn().mockResolvedValue(response);

return {
request: requestMock,
requestAbsolute: jest.fn(),
getBaseUrl: jest.fn().mockReturnValue('https://api.dfx.swiss'),
getApiUrl: jest.fn().mockReturnValue('https://api.dfx.swiss/v1'),
setToken: jest.fn(),
getToken: jest.fn(),
} as unknown as DfxHttpClient & { request: jest.Mock };
}

const requestBase = {
id: 'pl_1',
displayName: 'Shop',
standard: PaymentStandardType.OPEN_CRYPTO_PAY,
possibleStandards: [PaymentStandardType.OPEN_CRYPTO_PAY],
displayQr: true,
recipient: { name: 'Shop' },
mode: PaymentLinkMode.SINGLE,
transferAmounts: [],
};

describe('PaymentLinksApi pay flow', () => {
describe('getStandards', () => {
it('reads the public standard list without a token', async () => {
const mockHttp = createMockHttpClient([]);
const api = new PaymentLinksApi(mockHttp);

await api.getStandards();

expect(mockHttp.request).toHaveBeenCalledWith({
url: 'paymentLink/standard',
method: 'GET',
token: false,
});
});

it('reads a single standard by id', async () => {
const mockHttp = createMockHttpClient({});
const api = new PaymentLinksApi(mockHttp);

await api.getStandard(PaymentStandardType.PAY_TO_ADDRESS);

expect(mockHttp.request).toHaveBeenCalledWith({
url: 'paymentLink/standard/PayToAddress',
method: 'GET',
token: false,
});
});
});

describe('waitForPayment', () => {
it('passes the link identification as query parameters', async () => {
const mockHttp = createMockHttpClient({});
const api = new PaymentLinksApi(mockHttp);

await api.waitForPayment({ externalLinkId: 'ext 1', key: 'k' });

expect(mockHttp.request).toHaveBeenCalledWith({
url: 'paymentLink/payment/wait?externalLinkId=ext%201&key=k',
method: 'GET',
});
});

it('omits parameters that were not set', async () => {
const mockHttp = createMockHttpClient({});
const api = new PaymentLinksApi(mockHttp);

await api.waitForPayment({ linkId: '7' });

expect(mockHttp.request).toHaveBeenCalledWith({
url: 'paymentLink/payment/wait?linkId=7',
method: 'GET',
});
});
});

describe('confirmPayment', () => {
it('confirms via PUT', async () => {
const mockHttp = createMockHttpClient({});
const api = new PaymentLinksApi(mockHttp);

await api.confirmPayment({ externalPaymentId: 'p1', key: 'k' });

expect(mockHttp.request).toHaveBeenCalledWith({
url: 'paymentLink/payment/confirm?externalPaymentId=p1&key=k',
method: 'PUT',
});
});
});

describe('getHistory', () => {
it('sends statuses as a comma separated list and dates in ISO form', async () => {
const mockHttp = createMockHttpClient([]);
const api = new PaymentLinksApi(mockHttp);

await api.getHistory({
externalLinkId: 'ext1',
status: [PaymentLinkPaymentStatus.PENDING, PaymentLinkPaymentStatus.COMPLETED],
from: new Date('2026-01-01T00:00:00.000Z'),
});

expect(mockHttp.request).toHaveBeenCalledWith({
url: 'paymentLink/history?externalLinkId=ext1&status=Pending,Completed&from=2026-01-01T00%3A00%3A00.000Z',
method: 'GET',
});
});

it('sends no query at all for an empty filter', async () => {
const mockHttp = createMockHttpClient([]);
const api = new PaymentLinksApi(mockHttp);

await api.getHistory({});

expect(mockHttp.request).toHaveBeenCalledWith({ url: 'paymentLink/history', method: 'GET' });
});
});
});

describe('hasPaymentQuote', () => {
it('is true for a quoted pay request', () => {
const response: PaymentLinkPayResponse = {
...requestBase,
tag: 'payRequest',
callback: 'https://example.com/callback',
metadata: '[]',
minSendable: 1,
maxSendable: 2,
quote: { id: 'q1', expiration: new Date(), payment: 'pay1' },
requestedAmount: { asset: 'CHF', amount: 1 },
};

expect(hasPaymentQuote(response)).toBe(true);
});

it('is false for an idle terminal response', () => {
const response: PaymentLinkPayResponse = {
...requestBase,
error: 'Not Found',
message: 'No pending payment',
statusCode: 404,
};

expect(hasPaymentQuote(response)).toBe(false);
});
});
40 changes: 40 additions & 0 deletions packages/core/src/client/PaymentLinksApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ import {
UpdatePaymentLink,
AssignPaymentLink,
CreatePaymentLinkPayment,
PaymentLinkHistory,
PaymentLinkHistoryQuery,
PaymentLinkPaymentQuery,
PaymentLinkRecipient,
PaymentLinkConfig,
UpdatePaymentLinkConfig,
PaymentLinkPos,
PaymentStandard,
PaymentStandardType,
} from '../definitions/route';
import { CustomFile } from '../definitions/file';
import { Utils } from '../utils';
Expand Down Expand Up @@ -75,4 +80,39 @@ export class PaymentLinksApi {
const query = Utils.buildQuery(params);
return this.http.request<PaymentLinkPos>({ url: `${PaymentLinksUrl.pos}${query}`, method: 'PUT' });
}

/** The payment standards the API supports. Public - no session needed. */
async getStandards(): Promise<PaymentStandard[]> {
return this.http.request<PaymentStandard[]>({ url: PaymentLinksUrl.standard, method: 'GET', token: false });
}

async getStandard(id: PaymentStandardType): Promise<PaymentStandard> {
return this.http.request<PaymentStandard>({
url: PaymentLinksUrl.standardById(id),
method: 'GET',
token: false,
});
}

/**
* Long-polls until the payment reaches a final state, then returns the link with that payment.
*
* The request stays open for as long as the API keeps it open, and it resolves on a timeout as
* well - read the returned status instead of assuming the payment moved.
*/
async waitForPayment(params: PaymentLinkPaymentQuery): Promise<PaymentLink> {
const query = Utils.buildQuery({ ...params });
return this.http.request<PaymentLink>({ url: `${PaymentLinksUrl.paymentWait}${query}`, method: 'GET' });
}

async confirmPayment(params: PaymentLinkPaymentQuery): Promise<PaymentLink> {
const query = Utils.buildQuery({ ...params });
return this.http.request<PaymentLink>({ url: `${PaymentLinksUrl.paymentConfirm}${query}`, method: 'PUT' });
}

/** Payments of a link within a period. Without `status` the API returns completed payments only. */
async getHistory(params: PaymentLinkHistoryQuery): Promise<PaymentLinkHistory[]> {
const query = Utils.buildQuery({ ...params });
return this.http.request<PaymentLinkHistory[]>({ url: `${PaymentLinksUrl.history}${query}`, method: 'GET' });
}
}
16 changes: 16 additions & 0 deletions packages/core/src/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ export {
PaymentQuoteStatus,
MinCompletionStatus,
PaymentLinkBlockchain,
C2BPaymentMethod,
ManualPaymentMethod,
hasPaymentQuote,
} from './route';
export type {
MinAmount,
Expand All @@ -136,6 +139,19 @@ export type {
UpdatePaymentLink,
AssignPaymentLink,
PaymentLinkPos,
PaymentStandard,
TransferMethod,
TransferAmount,
PaymentAmount,
PaymentQuote,
PaymentLinkRequestBase,
PaymentLinkPayRequest,
PaymentLinkPayTerminal,
PaymentLinkPayResponse,
PaymentLinkHistory,
PaymentLinkHistoryPayment,
PaymentLinkPaymentQuery,
PaymentLinkHistoryQuery,
} from './route';
export { SellUrl } from './sell';
export type { Eip5792Call, Eip5792Data, UnsignedTx, Sell, Beneficiary, SellPaymentInfo, ConfirmSellData } from './sell';
Expand Down
Loading
Loading