Skip to content

Commit

Permalink
adds cancellation and success for radom
Browse files Browse the repository at this point in the history
  • Loading branch information
AlanBreck committed Oct 23, 2024
1 parent 2d99161 commit 8b8a08e
Show file tree
Hide file tree
Showing 13 changed files with 191 additions and 107 deletions.
8 changes: 4 additions & 4 deletions src/lib/payment-processing/checkoutSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ describe('Stripe', () => {
}
],
mode: 'payment',
success_url: 'http://localhost:5173/success/{CHECKOUT_SESSION_ID}/',
cancel_url: 'http://localhost:5173/cart/?session_id={CHECKOUT_SESSION_ID}',
success_url: 'http://localhost:5173/success/{CHECKOUT_SESSION_ID}/?provider=stripe',
cancel_url: 'http://localhost:5173/cart/?session_id={CHECKOUT_SESSION_ID}&provider=stripe',
allow_promotion_codes: true,
shipping_options: [
{
Expand Down Expand Up @@ -152,8 +152,8 @@ describe('Radom', () => {
]
}
},
successUrl: 'http://localhost:5173/success/{CHECKOUT_SESSION_ID}/',
cancelUrl: 'http://localhost:5173/cart/?session_id={CHECKOUT_SESSION_ID}',
successUrl: 'http://localhost:5173/success/{CHECKOUT_SESSION_ID}/?provider=radom',
cancelUrl: 'http://localhost:5173/cart/?session_id={CHECKOUT_SESSION_ID}&provider=radom',
chargeCustomerNetworkFee: true,
customizations: { allowDiscountCodes: true }
};
Expand Down
2 changes: 1 addition & 1 deletion src/lib/payment-processing/checkoutSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export async function formatCheckoutSessionParams<SessionCreateParams>(

const encryptedShippingData = await encryptShippingData(recipientInformation);

return providerAdapter(hydratedItems, encryptedShippingData, shippingRates);
return providerAdapter(hydratedItems, recipientInformation, encryptedShippingData, shippingRates);
}

// TODO: write tests for initCheckoutSession
Expand Down
36 changes: 29 additions & 7 deletions src/lib/payment-processing/providers/radom/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { env } from '$env/dynamic/private';
import { ENVIRONMENT } from '$env/static/private';
import { CANCELED_SESSION_QUERY_PARAM, PROVIDER_QUERY_PARAM } from '$lib/payment-processing/constants';
import {
CANCELED_SESSION_QUERY_PARAM,
PROVIDER_QUERY_PARAM
} from '$lib/payment-processing/constants';
import type { ProviderParamsAdapter, ShippingRate } from '$lib/payment-processing/types';
import { PROVIDER_NAME } from '.';
import { mainnetTokens, testnetTokens } from './tokens';
import type { Currency, Radom } from './types';

export const metadataKeys = {
ENCRYPTED_SHIPPING_DATA: 'encryptedShippingData',
SHIPPING_DATA_ENCRYPTION_KEY_ID: 'shippingDataEncryptionKeyId',
SHIPPING_RATE_ID: 'shippingRateId',
ITEM: 'item'
};

export const radomAdapter: ProviderParamsAdapter<Radom.Checkout.SessionCreateParams> = (
items,
recipient,
encryptedShippingAddress,
shippingRates
) => {
const lineItems: Array<Radom.Checkout.LineItem> = items.map((item) => {
const lineItems: Array<Radom.LineItem> = items.map((item) => {
return {
itemData: {
name: item.details.name,
Expand All @@ -32,27 +43,38 @@ export const radomAdapter: ProviderParamsAdapter<Radom.Checkout.SessionCreatePar
price: parseFloat(shipping.rate),
currency: shipping?.currency as Currency
}
})
});

return {
lineItems,
currency: 'USD',
gateway: {
managed: {
methods: ENVIRONMENT === "production" ? mainnetTokens(0.2) : testnetTokens(0.2)
methods: ENVIRONMENT === 'production' ? mainnetTokens(0.2) : testnetTokens(0.2)
}
},
successUrl: `${env.BASE_URL}/success/{CHECKOUT_SESSION_ID}/?${PROVIDER_QUERY_PARAM}=${PROVIDER_NAME}`,
cancelUrl: `${env.BASE_URL}/cart/?${CANCELED_SESSION_QUERY_PARAM}={CHECKOUT_SESSION_ID}&${PROVIDER_QUERY_PARAM}=radom`,
metadata: [
{
key: 'encryptedShippingData',
key: metadataKeys.ENCRYPTED_SHIPPING_DATA,
value: encryptedShippingAddress.encryptedData
},
{
key: 'shippingDataEncryptionKeyId',
key: metadataKeys.SHIPPING_DATA_ENCRYPTION_KEY_ID,
value: encryptedShippingAddress.encryptionKeyId
}
},
{
key: metadataKeys.SHIPPING_RATE_ID,
value: shipping.id
},
...items.map((i) => ({
key: metadataKeys.ITEM,
value: JSON.stringify({
id: i.printfulVariantId,
quantity: i.quantity
})
}))
],
chargeCustomerNetworkFee: true,
customizations: {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/payment-processing/providers/radom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export async function radomApi<T = any>(resourcePath: string, options?: RequestI
}
});

const responseBody = await response.json();
const responseBody =
parseInt(response.headers.get('content-length')) > 0 ? await response.json() : '';

if (response.status !== 200) {
error(400, responseBody);
Expand Down
6 changes: 3 additions & 3 deletions src/lib/payment-processing/providers/radom/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ export namespace Radom {
};

export namespace Checkout {
export type SessionStatus = 'pending' | 'success' | 'cancelled' | 'expired' | 'refunded';

export type Session = {
id: string;
sessionStatus: Status;
sessionStatus: SessionStatus;
organizationId: string;
products?: Product[];
items?: ItemData[];
Expand Down Expand Up @@ -60,8 +62,6 @@ export namespace Radom {
};
}

type Status = 'pending' | 'success' | 'cancelled' | 'expired' | 'refunded';

type Product = {
id: string;
organizationId: string;
Expand Down
2 changes: 2 additions & 0 deletions src/lib/payment-processing/providers/stripe/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ProviderParamsAdapter } from '../../types';

export const stripeAdapter: ProviderParamsAdapter<Stripe.Checkout.SessionCreateParams> = (
items,
recipient,
encryptedShippingAddress,
shippingRates
) => {
Expand Down Expand Up @@ -74,6 +75,7 @@ export const stripeAdapter: ProviderParamsAdapter<Stripe.Checkout.SessionCreateP
);

return {
customer_email: recipient.email,
line_items,
mode: 'payment',
success_url: `${env.BASE_URL}/success/{CHECKOUT_SESSION_ID}/?${PROVIDER_QUERY_PARAM}=${PROVIDER_NAME}`,
Expand Down
1 change: 1 addition & 0 deletions src/lib/payment-processing/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type HydratedCartItem = StrongVariant & {

export type ProviderParamsAdapter<SessionCreateParams> = (
items: HydratedCartItem[],
recipient: App.Recipient,
encryptedShippingData: EncryptedShippingAddress,
shippingRates: ShippingRate[]
) => SessionCreateParams;
Expand Down
2 changes: 1 addition & 1 deletion src/params/checkoutSessionId.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @type {import("@sveltejs/kit").ParamMatcher} */
export function match(param) {
return /^cs_[a-zA-Z0-9_-]+$/.test(param);
return /^(?:cs_)?[a-zA-Z0-9_-]+$/.test(param);
}
27 changes: 21 additions & 6 deletions src/routes/cart/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
createRadomCheckoutSession,
PROVIDER_NAME as RADOM_PROVIDER_NAME,
radomAdapter,
radomApi
radomApi,
type Radom
} from '$lib/payment-processing/providers/radom';
import {
stripe,
Expand All @@ -21,7 +22,6 @@ import { blockedCountryCodes } from '$lib/utils';
import type { CountryCallingCode, CountryCode } from 'libphonenumber-js';
import { getCountryCallingCode, isSupportedCountry } from 'libphonenumber-js/max';
import type { Actions, PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';

export type CartRequestBody = {
items: Array<{
Expand Down Expand Up @@ -66,20 +66,35 @@ export const load: PageServerLoad = async ({ url }) => {
try {
if (provider === STRIPE_PROVIDER_NAME) {
const session = await stripe.checkout.sessions.retrieve(canceledSession);
if (session.status === 'open') {
await stripe.checkout.sessions.expire(canceledSession);
}
if (session.metadata?.keyId) {
await sdk.DeleteShippingDataKey({ id: session.metadata?.keyId });
}
} else if (provider === RADOM_PROVIDER_NAME) {
const session = await radomApi<{ metadata: [{ key: string; value: string }] }>(
`/checkout_session/${canceledSession}`
);
const session = await radomApi<{
metadata: [{ key: string; value: string }];
sessionStatus: Radom.Checkout.SessionStatus;
}>(`/checkout_session/${canceledSession}`);
if (session.sessionStatus === "pending") {
await radomApi(`/checkout_session/${canceledSession}/cancel`, {
method: 'POST'
});
}
const keyId = session.metadata.find((v) => v.key === 'shippingDataEncryptionKeyId')?.value;
if (keyId) {
await sdk.DeleteShippingDataKey({ id: keyId });
}
}
} catch (e: any) {
console.log(`Could not delete shipping data key due to error: ${e.message}`);
if (e?.message?.includes('ShippingDataKey')) {
console.error(
`Could not delete shipping data key due to error: ${e?.response?.errors[0]?.message}`
);
} else {
console.error(e);
}
}
}

Expand Down
21 changes: 17 additions & 4 deletions src/routes/cart/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,27 @@
{#if showShippingAddress}
<div transition:slide|local class="shipping_address">
<h3 class="text-default-semibold pb-xl">Shipping address</h3>
<div class="form-control">
<label for="shippingAddress[name]"
>Name <span class="label-explanation">(if different from billing name)</span></label
>
<div class="form-control required">
<label for="shippingAddress[name]">Name</label>
<input
value={form?.values?.name || ''}
name="shippingAddress[name]"
id="shippingAddress[name]"
type="text"
placeholder="Jane Smith"
required
/>
</div>

<div class="form-control required">
<label for="shippingAddress[email]">Email address</label>
<input
value={form?.values?.email || ''}
name="shippingAddress[email]"
id="shippingAddress[email]"
type="email"
placeholder="janesmith@example.com"
required
/>
</div>

Expand Down Expand Up @@ -408,6 +419,7 @@
}
&.errors input[type='text'],
&.errors input[type='email'],
&.errors select {
--bg: theme('colors.systemfeedback.error-background');
}
Expand Down Expand Up @@ -470,6 +482,7 @@
}
input[type='text'],
input[type='email'],
select {
@apply text-default-regular;
Expand Down
97 changes: 64 additions & 33 deletions src/routes/success/[sessionId=checkoutSessionId]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { redirect } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import { stripe } from '$lib/payment-processing/providers/stripe';
import type Stripe from 'stripe';
import type { PageServerLoad } from './$types';
import { sdk } from '$lib/graphql/sdk';
import {
PROVIDER_NAME as RADOM_PROVIDER_NAME,
radomApi,
type Radom
} from '$lib/payment-processing/providers/radom';
import {
stripe,
PROVIDER_NAME as STRIPE_PROVIDER_NAME
} from '$lib/payment-processing/providers/stripe';
import * as Sentry from '@sentry/node';
import { redirect } from '@sveltejs/kit';
import type Stripe from 'stripe';
import type { PageServerLoad } from './$types';

// TODO: Implement cancel logic
// Add param to distinguish between Stripe and Radom
Expand All @@ -20,47 +28,70 @@ Sentry.init({
}
});

export const load: PageServerLoad = async function load({ params }) {
export const load: PageServerLoad = async function load({ params, url }) {
const provider = url.searchParams.get('provider');
try {
const session = await stripe.checkout.sessions.retrieve(params.sessionId, {
expand: ['line_items.data.price.product']
});
let purchasedVariants: { quantity: number; id: string }[] = [];

/**
* Determine if webhook associated with checkout session was successful
*/
const events = await stripe.events.list({
type: 'checkout.session.completed',
created: {
gte: session.created
}
});
if (!provider) {
throw new Error('No provider was specified.');
} else if (provider === STRIPE_PROVIDER_NAME) {
const session = await stripe.checkout.sessions.retrieve(params.sessionId, {
expand: ['line_items.data.price.product']
});

/**
* Determine if webhook associated with checkout session was successful
*/
const events = await stripe.events.list({
type: 'checkout.session.completed',
created: {
gte: session.created
}
});

const sessionEvents = events.data.filter((e) => {
const resource = e.data.object as Stripe.Checkout.Session;
return resource?.id === session.id;
});
const sessionEvents = events.data.filter((e) => {
const resource = e.data.object as Stripe.Checkout.Session;
return resource?.id === session.id;
});

// If most recent event (index 0) has pending_webhooks, trigger alert.
if (sessionEvents[0]?.pending_webhooks > 0) {
console.log('Pending webhooks: ', sessionEvents[0].pending_webhooks);
Sentry.captureMessage(
`Customer order not submitted to Printful ${session.payment_intent}`,
'error'
// If most recent event (index 0) has pending_webhooks, trigger alert.
if (sessionEvents[0]?.pending_webhooks > 0) {
console.log('Pending webhooks: ', sessionEvents[0].pending_webhooks);
Sentry.captureMessage(
`Customer order not submitted to Printful ${session.payment_intent}`,
'error'
);
}

purchasedVariants =
session.line_items?.data?.map((li) => {
const product = li.price?.product as Stripe.Product;
const variantId = product.metadata.printfulVariantId;
return {
quantity: li.quantity,
id: variantId
};
}) ?? [];
} else if (provider === RADOM_PROVIDER_NAME) {
const session: Radom.Checkout.Session = await radomApi(
`/checkout_session/${params.sessionId}`
);

purchasedVariants = session.metadata
.filter((s) => s.key === 'item')
.map((item) => JSON.parse(item.value));
}

const items = await Promise.all(
session.line_items?.data?.map(async (li) => {
const product = li.price?.product as Stripe.Product;
const variantId = product.metadata.printfulVariantId;
const { variants } = await sdk.Variant({ printfulId: variantId });
purchasedVariants.map(async (variant) => {
const { variants } = await sdk.Variant({ printfulId: variant.id });

return {
quantity: li.quantity,
quantity: variant.quantity,
product: variants?.at(0)
};
}) ?? []
})
);

return {
Expand Down
Loading

0 comments on commit 8b8a08e

Please sign in to comment.