Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the latest types from paypal-js #37

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ To migrate from a client-side to a server-side integration, you will first need
1. Setup a server-side integration with the [PayPal REST APIs](https://developer.paypal.com/api/rest/).

1. `createOrder` JavaScript callback must be changed to use your server to create and return an order ID using the PayPal
REST API's.
REST API's.

1. `onApprove` JavaScript callback must be changed to use your server to complete transactions using the PayPal REST APIs.

Expand Down Expand Up @@ -84,17 +84,17 @@ createOrder: function (data, actions) {

To simplify the integration of your e-commerce website with the PayPal v2 Orders API, you can move the order creation process to your server-side. The following steps are required to create an order on the server-side:

1. Obtain an access token to use for PayPal backend API calls. This [video tutorial](https://www.youtube.com/watch?v=HOkkbGSxmp4&t=113s) can walk you through the steps.
2. Pass necessary checkout information from the browser client to your server-side API endpoint.
3. Call the PayPal Orders API from your server-side code and return the order ID in your `createOrder()` callback.
4. If you were assigned a `BN Code` for your integration, be sure to include this value in the `PayPal-Partner-Attribution-Id` header of the server-side Create Order API call.
1. Obtain an access token to use for PayPal backend API calls. This [video tutorial](https://www.youtube.com/watch?v=HOkkbGSxmp4&t=113s) can walk you through the steps.
2. Pass necessary checkout information from the browser client to your server-side API endpoint.
3. Call the PayPal Orders API from your server-side code and return the order ID in your `createOrder()` callback.
4. If you were assigned a `BN Code` for your integration, be sure to include this value in the `PayPal-Partner-Attribution-Id` header of the server-side Create Order API call.
5. If you are making the server-side Create Order API call on behalf of a connected merchant, you will need to include the [PayPal-Auth-Assertion](https://developer.paypal.com/api/rest/requests/#http-request-headers) header or alternatively, pass the merchant's PayPal Account ID in the [payee](https://developer.paypal.com/docs/api/orders/v2/#orders_create!path=purchase_units/payee/merchant_id&t=request) field of the Create Order `purchase_unit`.

<br/>

**_Helpful diagram highlighting the sequence of events required for a client + server integration for creating and returning an order ID:_**

```mermaid
```mermaid
sequenceDiagram
actor Buyer
participant M(HTML) as Partner's HTML Page
Expand All @@ -110,7 +110,9 @@ sequenceDiagram
PP(ORDER)->>M(S): Order Created
M(S)->>M(HTML): Return Order ID
```

**_Sample Create Order API request:_**

```
curl -v -X POST https://api-m.sandbox.paypal.com/v2/checkout/orders \
-H 'Content-Type: application/json' \
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"license": "ISC",
"dependencies": {
"@fastify/static": "^6.9.0",
"@paypal/paypal-js": "^5.1.5",
"@paypal/paypal-js": "^8.0.0-beta.5",
"dotenv": "^16.0.3",
"fastify": "^4.15.0",
"pino": "^8.11.0",
Expand Down
4 changes: 3 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// add a ".env" file to your project to set these environment variables
import { INTENT } from "@paypal/paypal-js";
import { CreateOrderRequestBody } from "@paypal/paypal-js";
import * as dotenv from "dotenv";

type INTENT = CreateOrderRequestBody["intent"];

if (process.env.NODE_ENV !== "production") {
dotenv.config();
}
Expand Down
43 changes: 27 additions & 16 deletions src/controller/order-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ import createOrder from "../order/create-order";
import captureOrder from "../order/capture-order";
import getOrder from "../order/get-order";
import patchOrder from "../order/patch-order";
import type { PatchOrderResponse, PatchRequest } from "../order/patch-order";
import type {
PatchOrderResponse,
PatchOrderOptions,
} from "../order/patch-order";
import products from "../data/products.json";
import shippingCost from "../data/shipping-cost.json";
import config from "../config";

import type {
CreateOrderRequestBody,
OrderResponseBody,
PurchaseItem,
ShippingAddress,
OrderSuccessResponseBody,
PurchaseUnitItem,
OnShippingChangeData,
} from "@paypal/paypal-js";

// TODO: update front-end to use the new onShippingAddressChange() callback
type PartialShippingAddress = NonNullable<
OnShippingChangeData["shipping_address"]
>;

const {
paypal: { currency, intent },
} = config;
Expand All @@ -30,7 +38,7 @@ function roundTwoDecimals(value: number): number {
}

function getItemsAndTotal(cart: CartItem[]): {
itemsArray: PurchaseItem[];
itemsArray: PurchaseUnitItem[];
itemTotal: number;
} {
// API reference: https://developer.paypal.com/docs/api/orders/v2/#orders_create!path=purchase_units/items&t=request
Expand All @@ -50,7 +58,7 @@ function getItemsAndTotal(cart: CartItem[]): {
currency_code: currency,
value: price,
},
} as PurchaseItem;
} as PurchaseUnitItem;
});

const itemTotal = itemsArray.reduce(
Expand Down Expand Up @@ -137,7 +145,7 @@ async function createOrderHandler(
});

if (orderResponse.status === "ok") {
const { id, status } = orderResponse.data as OrderResponseBody;
const { id, status } = orderResponse.data as OrderSuccessResponseBody;
request.log.info({ id, status }, "order successfully created");
} else {
request.log.error(orderResponse.data, "failed to create order");
Expand All @@ -153,7 +161,7 @@ async function captureOrderHandler(
const { orderID } = request.body as { orderID: string };

const responseData = await captureOrder(orderID);
const data = responseData?.data as OrderResponseBody;
const data = responseData?.data as OrderSuccessResponseBody;
const transaction =
data?.purchase_units?.[0]?.payments?.captures?.[0] ||
data?.purchase_units?.[0]?.payments?.authorizations?.[0];
Expand Down Expand Up @@ -224,10 +232,11 @@ export async function captureOrderController(fastify: FastifyInstance) {
}

// Return the shipping cost for an address (can be "0"), or false to reject shipping to that address
function calcShipping(address: ShippingAddress): string | boolean {
function calcShipping(address: PartialShippingAddress): string | boolean {
const prices = shippingCost as {
[key: string]: { [key: string]: string | boolean };
};

const country = address?.country_code;
const state = address?.state;
return (
Expand All @@ -240,17 +249,18 @@ function calcShipping(address: ShippingAddress): string | boolean {

async function onShippingChange(
orderID: string,
shippingAddress: ShippingAddress
shippingAddress: PartialShippingAddress
): Promise<PatchOrderResponse> {
if (!orderID) {
throw new Error("MISSING_ORDER_ID_FOR_PATCH_ORDER");
}

// const defaultErrorMessage = "FAILED_TO_PATCH_ORDER";

const patchOps: PatchRequest[] = [];
const patchOps: PatchOrderOptions["body"] = [];
// get the current details
const orderDetails = (await getOrder({ orderID })).data as OrderResponseBody;
const orderDetails = (await getOrder({ orderID }))
.data as OrderSuccessResponseBody;

// Loop over the order purchase_units array; most use cases should only have one
orderDetails?.purchase_units?.forEach((pu) => {
Expand All @@ -265,7 +275,7 @@ async function onShippingChange(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
pu!.amount!.breakdown!.shipping = {
value: shipping.toString(),
currency_code: pu.amount.currency_code || "USD",
currency_code: pu?.amount?.currency_code || "USD",
};

/* Similarly you could have a new tax calculation, etc
Expand Down Expand Up @@ -294,17 +304,18 @@ async function onShippingChange(
patchOps.push({
op: "replace",
path: `/purchase_units/@reference_id=='${reference_id}'/amount`,
value: pu.amount,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
value: pu!.amount!,
});
}); // loop over next purchase_unit, if there is one (rare use case)

return patchOrder({ body: { patch_request: patchOps }, orderID });
return patchOrder({ body: patchOps, orderID });
}

async function patchOrderHandler(request: FastifyRequest, reply: FastifyReply) {
const { orderID, shippingAddress } = request.body as {
orderID: string;
shippingAddress: ShippingAddress;
shippingAddress: PartialShippingAddress;
};

const patchOrderResponse = await onShippingChange(orderID, shippingAddress);
Expand Down
5 changes: 4 additions & 1 deletion src/controller/subscription-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ async function createSubscriptionHandler(
reply: FastifyReply
) {
const { userAction = "SUBSCRIBE_NOW" } = request.body as {
userAction: string;
userAction: "SUBSCRIBE_NOW" | "CONTINUE";
};

/**
* To create subscription, you must create a product and a subscription plan.
* Steps:
Expand All @@ -35,6 +36,8 @@ async function createSubscriptionHandler(
plan_id: String(subscriptionPlanId),
application_context: {
user_action: userAction,
return_url: "",
cancel_url: "",
},
};
const { data } = await createSubscription(body);
Expand Down
61 changes: 34 additions & 27 deletions src/order/capture-order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,39 @@ import config from "../config";
import getAuthToken from "../auth/get-auth-token";

import type {
OrderResponseBody,
OrderResponseBodyMinimal,
OrderSuccessResponseBody,
OrderSuccessResponseBodyMinimal,
OrderErrorResponseBody,
CaptureOrder,
} from "@paypal/paypal-js";
import type {
CreateCaptureHTTPStatusCodeSuccessResponse,
OrderErrorResponse,
OrderResponse,
} from "./order";
import type { HttpErrorResponse } from "../types/common";

const {
paypal: { apiBaseUrl },
} = config;

type CaptureOrderRequestBody = {
[key: string]: unknown;
payment_source?: { [key: string]: unknown };
type CaptureOrderRequestBody = NonNullable<
CaptureOrder["requestBody"]
>["content"]["application/json"];

// the Authorization header is missing from the headers list
type CaptureOrderRequestHeaders = Partial<
CaptureOrder["parameters"]["header"]
> & {
Authorization?: string;
};

type CaptureOrderSuccessResponse = {
status: "ok";
data: OrderSuccessResponseBody | OrderSuccessResponseBodyMinimal;
httpStatusCode: 200 | 201;
};

type CaptureOrderRequestHeaders = Partial<{
"PayPal-Auth-Assertion": string;
"PayPal-Client-Metadata-Id": string;
"PayPal-Request-Id": string;
Prefer: string;
Authorization: string;
"Content-Type": string;
}>;
type CaptureOrderErrorResponse = {
status: "error";
data: OrderErrorResponseBody;
httpStatusCode: number;
};

type CaptureOrderOptions = {
body?: CaptureOrderRequestBody;
Expand All @@ -40,7 +46,7 @@ type CaptureOrderOptions = {

export default async function captureOrder(
orderID: string
): Promise<OrderResponse> {
): Promise<CaptureOrderSuccessResponse | CaptureOrderErrorResponse> {
const uniqueRequestId: string = randomUUID();
// Call the API
let responseData = await captureOrderAPI({
Expand All @@ -67,7 +73,9 @@ async function captureOrderAPI({
orderID,
body,
headers,
}: CaptureOrderOptions): Promise<OrderResponse> {
}: CaptureOrderOptions): Promise<
CaptureOrderSuccessResponse | CaptureOrderErrorResponse
> {
if (!orderID) {
throw new Error("MISSING_ORDER_ID_FOR_CAPTURE_ORDER");
}
Expand Down Expand Up @@ -103,17 +111,16 @@ async function captureOrderAPI({
status: "ok",
data:
requestHeaders.Prefer === "return=minimal"
? (data as OrderResponseBodyMinimal)
: (data as OrderResponseBody),
httpStatusCode:
response.status as CreateCaptureHTTPStatusCodeSuccessResponse,
};
? (data as OrderSuccessResponseBodyMinimal)
: (data as OrderSuccessResponseBody),
httpStatusCode: response.status,
} as CaptureOrderSuccessResponse;
} else {
return {
status: "error",
data: data as OrderErrorResponse,
data,
httpStatusCode: response.status,
};
} as CaptureOrderErrorResponse;
}
} catch (error) {
const httpError: HttpErrorResponse =
Expand Down
Loading