Skip to content

Commit

Permalink
Add Charge api (#8)
Browse files Browse the repository at this point in the history
* feat: Add Recurring Charge API
* tests: Improve test coverage
  • Loading branch information
tomas-zijdemans-vipps authored Dec 21, 2023
1 parent 38b59dd commit 4d48e9f
Show file tree
Hide file tree
Showing 16 changed files with 927 additions and 106 deletions.
107 changes: 107 additions & 0 deletions sample_code/charge_sample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import "https://deno.land/std@0.209.0/dotenv/load.ts";
import { Client } from "../src/mod.ts";

// First, get your API keys from https://portal.vipps.no/
// Here we assume they are stored in a .env file, see .env.example
const clientId = Deno.env.get("CLIENT_ID") || "";
const clientSecret = Deno.env.get("CLIENT_SECRET") || "";

const merchantSerialNumber = Deno.env.get("MERCHANT_SERIAL_NUMBER") || "";
const subscriptionKey = Deno.env.get("SUBSCRIPTION_KEY") || "";

const customerPhoneNumber = "4791234567";

// Create a client
const client = Client({
merchantSerialNumber,
subscriptionKey,
useTestMode: true,
retryRequests: false,
});

// Grab a token
const accessToken = await client.auth.getToken({
clientId,
clientSecret,
subscriptionKey,
});

// Check if the token was retrieved successfully
if (!accessToken.ok) {
console.error("😟 Error retrieving token 😟");
console.error(accessToken.message);
Deno.exit(1);
}

const token = accessToken.data.access_token;

const agreement = await client.agreement.create(token, {
pricing: {
type: "LEGACY",
amount: 2500,
currency: "NOK",
},
interval: {
unit: "MONTH",
count: 1,
},
merchantRedirectUrl: "https://example.com/redirect",
merchantAgreementUrl: "https://example.com/agreement",
phoneNumber: customerPhoneNumber,
productName: "MyNews Digital",
});

// Check if the agreement was created successfully
if (!agreement.ok) {
console.error("😟 Error creating agreement 😟");
console.error(agreement.error);
Deno.exit(1);
}

const agreementId = agreement.data.agreementId;

const acceptedAgreement = await client.agreement.forceAccept(
token,
agreementId,
{ phoneNumber: customerPhoneNumber },
);

// Check if the agreement was accepted successfully
if (!acceptedAgreement.ok) {
console.error("😟 Error accepting agreement 😟");
console.error(acceptedAgreement.error);
Deno.exit(1);
}

// 10 days from now in YYYY-MM-DD format
const tenDaysFromToday = new Date(Date.now() + 10 * 24 * 60 * 60 * 1000)
.toISOString().split("T")[0];

const charge = await client.charge.create(token, agreementId, {
amount: 2500,
description: "MyNews Digital",
orderId: crypto.randomUUID(),
due: tenDaysFromToday,
retryDays: 5,
transactionType: "DIRECT_CAPTURE",
});

// Check if the charge was created successfully
if (!charge.ok) {
console.error("😟 Error creating charge 😟");
console.error(charge.error);
Deno.exit(1);
}

const chargeId = charge.data.chargeId;

const chargeInfo = await client.charge.info(token, agreementId, chargeId);

// Check if the charge info was fetched successfully
if (!chargeInfo.ok) {
console.error("😟 Error retrieving charge 😟");
console.error(chargeInfo.error);
Deno.exit(1);
}

console.log(chargeInfo.data);
4 changes: 2 additions & 2 deletions scripts/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ if (!branchTotal) {
}

if (branchTotal > THRESHOLD) {
console.log(`Branch coverage is good: ${branchTotal}`);
console.log(`Branch coverage is good: ${branchTotal}%`);
} else {
console.log(`Branch coverage is bad: ${branchTotal}`);
console.log(`Branch coverage is bad: ${branchTotal}%`);
Deno.exit(1);
}
12 changes: 6 additions & 6 deletions src/apis/agreement.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { RequestData } from "../types.ts";
import {
AgreementErrorResponse,
AgreementResponseV3,
AgreementStatus,
DraftAgreementResponseV3,
DraftAgreementV3,
ForceAcceptAgreementV3,
PatchAgreementV3,
} from "./types/agreement_types.ts";
import { RecurringErrorResponse } from "./types/recurring_types.ts";

/**
* Factory object for creating and managing agreements.
Expand Down Expand Up @@ -36,7 +36,7 @@ export const agreementRequestFactory = {
create(
token: string,
body: DraftAgreementV3,
): RequestData<DraftAgreementResponseV3, AgreementErrorResponse> {
): RequestData<DraftAgreementResponseV3, RecurringErrorResponse> {
return {
url: "/recurring/v3/agreements",
method: "POST",
Expand All @@ -59,7 +59,7 @@ export const agreementRequestFactory = {
token: string,
status: AgreementStatus,
createdAfter: number,
): RequestData<AgreementResponseV3, AgreementErrorResponse> {
): RequestData<AgreementResponseV3, RecurringErrorResponse> {
return {
url:
`/recurring/v3/agreements?status=${status}&createdAfter=${createdAfter}`,
Expand All @@ -78,7 +78,7 @@ export const agreementRequestFactory = {
info(
token: string,
agreementId: string,
): RequestData<AgreementResponseV3, AgreementErrorResponse> {
): RequestData<AgreementResponseV3, RecurringErrorResponse> {
return {
url: `/recurring/v3/agreements/${agreementId}`,
method: "GET",
Expand All @@ -99,7 +99,7 @@ export const agreementRequestFactory = {
token: string,
agreementId: string,
body: PatchAgreementV3,
): RequestData<void, AgreementErrorResponse> {
): RequestData<void, RecurringErrorResponse> {
return {
url: `/recurring/v3/agreements/${agreementId}`,
method: "PATCH",
Expand All @@ -120,7 +120,7 @@ export const agreementRequestFactory = {
token: string,
agreementId: string,
body: ForceAcceptAgreementV3,
): RequestData<void, AgreementErrorResponse> {
): RequestData<void, RecurringErrorResponse> {
return {
url: `/recurring/v3/agreements/${agreementId}/accept`,
method: "PATCH",
Expand Down
167 changes: 167 additions & 0 deletions src/apis/charge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { RequestData } from "../types.ts";
import {
ChargeReference,
ChargeResponseV3,
ChargeStatus,
CreateChargeV3,
ModifyCharge,
} from "./types/charge_types.ts";
import { RecurringErrorResponse } from "./types/recurring_types.ts";

/**
* Factory object for managing charge API requests.
*/
export const chargeRequestFactory = {
/**
* Creates a new recurring charge (payment) that will charge the user
* on the date specified. If the payment fails,
* the charge will be retried based on retryDays
*
* @param token - The authentication token.
* @param body - The request body containing the charge details.
* @returns A RequestData object containing the URL, method, and token for the API request.
*/
create(
token: string,
agreementId: string,
body: CreateChargeV3,
): RequestData<ChargeReference, RecurringErrorResponse> {
return {
url: `/recurring/v3/agreements/${agreementId}/charges`,
method: "POST",
body,
token,
};
},
/**
* Fetches a single charge for a user.
*
* @param token - The authentication token.
* @param chargeId - The ID of the charge to retrieve.
* @param agreementId - The ID of the agreement.
* @returns A RequestData object containing the URL, method, and token for the API request.
*/
info(
token: string,
agreementId: string,
chargeId: string,
): RequestData<ChargeResponseV3, RecurringErrorResponse> {
return {
url: `/recurring/v3/agreements/${agreementId}/charges/${chargeId}`,
method: "GET",
token,
};
},
/**
* Retrieves information about a charge by its ID. A "special case"
* endpoint to fetch a single charge just by chargeId, when the
* agreementId is unknown. This is useful for investigating
* claims from customers, but not intended for automation.
*
* Please note: This is not a replacement for the normal endpoint
* for fetching charges
*
* @param token - The access token.
* @param chargeId - The ID of the charge.
* @returns A `RequestData` object containing the URL, method, and token.
*/
infoById(
token: string,
chargeId: string,
): RequestData<ChargeResponseV3, RecurringErrorResponse> {
return {
url: `/recurring/v3/agreements/charges/${chargeId}`,
method: "GET",
token,
};
},
/**
* Fetches all charges for a single agreement, including the optional
* initial charge. Supports filtering on status using query parameter.
*
* @param token - The authentication token.
* @param agreementId - The ID of the agreement.
* @returns A RequestData object containing the URL, method, and token for the API request.
*/
list(
token: string,
agreementId: string,
status?: ChargeStatus,
): RequestData<ChargeResponseV3[], RecurringErrorResponse> {
const url = status
? `/recurring/v3/agreements/${agreementId}/charges?status=${status}`
: `/recurring/v3/agreements/${agreementId}/charges`;

return { url, method: "GET", token };
},
/**
* Cancels a pending, due or reserved charge. When cancelling a charge
* that is PARTIALLY_CAPTURED, the remaining funds on the charge
* will be released back to the customer.
*
* Note if you cancel an agreement, there is no need to cancel the
* charges that belongs to the agreement. This will be done automatically.
*
* @param token - The authentication token.
* @param agreementId - The ID of the agreement.
* @param chargeId - The ID of the charge.
* @returns A RequestData object containing the URL, method, and token for the API request.
*/
cancel(
token: string,
agreementId: string,
chargeId: string,
): RequestData<void, RecurringErrorResponse> {
return {
url: `/recurring/v3/agreements/${agreementId}/charges/${chargeId}`,
method: "DELETE",
token,
};
},
/**
* Captures a reserved charge. Only charges with transactionType
* RESERVE_CAPTURE can be captured. Can also do partial captures
* (captures a smaller part of the payment).
*
* @param token - The authentication token.
* @param agreementId - The ID of the agreement.
* @param chargeId - The ID of the charge.
* @returns A RequestData object containing the URL, method, and token for the API request.
*/
capture(
token: string,
agreementId: string,
chargeId: string,
body: ModifyCharge,
): RequestData<void, RecurringErrorResponse> {
return {
url:
`/recurring/v3/agreements/${agreementId}/charges/${chargeId}/capture`,
method: "POST",
body,
token,
};
},
/**
* Refunds a charge, can also do a partial refund
* (refunding a smaller part of the payment).
*
* @param token - The authentication token.
* @param agreementId - The ID of the agreement.
* @param chargeId - The ID of the charge.
* @returns A RequestData object containing the URL, method, and token for the API request.
*/
refund(
token: string,
agreementId: string,
chargeId: string,
body: ModifyCharge,
): RequestData<void, RecurringErrorResponse> {
return {
url: `/recurring/v3/agreements/${agreementId}/charges/${chargeId}/refund`,
method: "POST",
body,
token,
};
},
} as const;
2 changes: 1 addition & 1 deletion src/apis/epayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "./types/epayment_types.ts";

/**
* Factory object for creating ePayment request data.
* Factory object for creating ePayment API requests.
*/
export const ePaymentRequestFactory = {
/**
Expand Down
Loading

0 comments on commit 4d48e9f

Please sign in to comment.