From 597d42051742456bb26bc03b3782682e2226fce7 Mon Sep 17 00:00:00 2001 From: Andrea Diotallevi Date: Thu, 22 Aug 2024 20:20:05 +0300 Subject: [PATCH 1/7] feat: log event --- serverless/src/handlers/theprintspaceHandleWebhooks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/serverless/src/handlers/theprintspaceHandleWebhooks.ts b/serverless/src/handlers/theprintspaceHandleWebhooks.ts index 71c87e6..753506c 100644 --- a/serverless/src/handlers/theprintspaceHandleWebhooks.ts +++ b/serverless/src/handlers/theprintspaceHandleWebhooks.ts @@ -1,3 +1,4 @@ export const handler = async (event: unknown) => { + console.log(event) console.log(JSON.stringify(event)) } From 25112dd602a2b5723a66f725bbdccdbb7b691e59 Mon Sep 17 00:00:00 2001 From: Andrea Diotallevi Date: Thu, 22 Aug 2024 20:22:52 +0300 Subject: [PATCH 2/7] infra: add resources for theprintspace create order function and queues --- serverless/template.yaml | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/serverless/template.yaml b/serverless/template.yaml index b5ec599..a98e566 100644 --- a/serverless/template.yaml +++ b/serverless/template.yaml @@ -63,6 +63,8 @@ Resources: Id: SendOrderConfirmationEmailQueueTarget - Arn: !GetAtt ProdigiCreateOrderQueue.Arn Id: ProdigiCreateOrderQueueTarget + - Arn: !GetAtt TheprintspaceCreateOrderQueue.Arn + Id: TheprintspaceCreateOrderQueueTarget ProdigiOrderShippedRule: Type: AWS::Events::Rule @@ -138,6 +140,35 @@ Resources: QueueName: !Sub ${AWS::StackName}-ProdigiCreateOrderDLQ MessageRetentionPeriod: 1209600 + TheprintspaceCreateOrderQueuePolicy: + Type: AWS::SQS::QueuePolicy + Properties: + PolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: events.amazonaws.com + Action: sqs:SendMessage + Resource: !GetAtt TheprintspaceCreateOrderQueue.Arn + Queues: + - Ref: TheprintspaceCreateOrderQueue + + TheprintspaceCreateOrderQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: !Sub ${AWS::StackName}-TheprintspaceCreateOrderQueue + VisibilityTimeout: 100 + ReceiveMessageWaitTimeSeconds: 20 + RedrivePolicy: + deadLetterTargetArn: !GetAtt TheprintspaceCreateOrderDLQ.Arn + maxReceiveCount: 1 + + TheprintspaceCreateOrderDLQ: + Type: AWS::SQS::Queue + Properties: + QueueName: !Sub ${AWS::StackName}-TheprintspaceCreateOrderDLQ + MessageRetentionPeriod: 1209600 + SendOrderShippedEmailQueuePolicy: Type: AWS::SQS::QueuePolicy Properties: @@ -691,3 +722,35 @@ Resources: Target: "es2020" EntryPoints: - src/handlers/theprintspaceHandleWebhooks.ts + + TheprintspaceCreateOrderFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub ${AWS::StackName}-TheprintspaceCreateOrder + Handler: src/handlers/theprintspaceCreateOrder.handler + Events: + MySQSEvent: + Type: SQS + Properties: + Queue: !GetAtt TheprintspaceCreateOrderQueue.Arn + BatchSize: 5 + Environment: + Variables: + CREATIVEHUB_API_URL: "{{resolve:ssm:CREATIVEHUB_API_URL}}" + ENVIRONMENT: !Ref Environment + Policies: + - Statement: + - Sid: readParameterStore + Effect: Allow + Action: + - ssm:GetParameter + Resource: + - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/CREATIVEHUB_API_KEY + - !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/STRIPE_SECRET_KEY + Metadata: + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + EntryPoints: + - src/handlers/theprintspaceCreateOrder.ts From 08db12ec5b23dcb384c2b146fbc3e593faf9f313 Mon Sep 17 00:00:00 2001 From: Andrea Diotallevi Date: Thu, 22 Aug 2024 20:23:37 +0300 Subject: [PATCH 3/7] feat: create handler for creating an order with theprintspace --- .../src/handlers/theprintspaceCreateOrder.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 serverless/src/handlers/theprintspaceCreateOrder.ts diff --git a/serverless/src/handlers/theprintspaceCreateOrder.ts b/serverless/src/handlers/theprintspaceCreateOrder.ts new file mode 100644 index 0000000..3f4dd47 --- /dev/null +++ b/serverless/src/handlers/theprintspaceCreateOrder.ts @@ -0,0 +1,35 @@ +import { SQSEvent } from "aws-lambda" +import Stripe from "stripe" + +import { + createConfirmedOrder, + createEmbryonicOrder, +} from "../services/theprintspace" +import { retrieveCheckoutSession } from "../services/stripe" + +export const handler = async (event: SQSEvent): Promise => { + try { + for (const record of event.Records) { + const body = JSON.parse(record.body) + const event = body.detail as Stripe.CheckoutSessionCompletedEvent + const sessionId = event.data.object.id + const { session } = await retrieveCheckoutSession({ sessionId }) + + console.log(JSON.stringify(session)) + + if (process.env.ENVIRONMENT !== "production") { + // Required until Creativehub updates the SSL certificate for the sandbox API environment (which has expired) + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" + } + + const { orderId, deliveryOptions } = await createEmbryonicOrder({ + session, + }) + + await createConfirmedOrder({ orderId, deliveryOptions }) + } + } catch (error) { + console.error(error) + throw error + } +} From 9dea0b53eb076f8cf42dc0261b19e0686423b5b3 Mon Sep 17 00:00:00 2001 From: Andrea Diotallevi Date: Thu, 22 Aug 2024 20:24:07 +0300 Subject: [PATCH 4/7] feat: amend creativehub API functions --- serverless/src/services/theprintspace.ts | 165 ++++++++++++++--------- 1 file changed, 100 insertions(+), 65 deletions(-) diff --git a/serverless/src/services/theprintspace.ts b/serverless/src/services/theprintspace.ts index f801eae..71bb529 100644 --- a/serverless/src/services/theprintspace.ts +++ b/serverless/src/services/theprintspace.ts @@ -1,94 +1,129 @@ import Stripe from "stripe" + import { getParameterValue } from "./ssm" + import { getCountryIdFromCountryCode, getCountryNameFromCountryCode, } from "../data/theprintspace" +import { + CreateConfirmedOrderResponse, + CreateEmbryonicOrderResponse, +} from "../types/theprintspace" + export const createEmbryonicOrder = async ({ session, }: { session: Stripe.Checkout.Session }) => { - try { - const { shipping_details, customer_details, line_items, id } = session + const { shipping_details, customer_details, line_items, id } = session - if (!shipping_details?.address) { - throw new Error("No shipping details") - } - if (!shipping_details.address.country) { - throw new Error("No shipping address country") - } - if (!customer_details) { - throw new Error("No customer details") - } - if (!line_items) { - throw new Error("No line items") - } - - const creativehubApiKey = await getParameterValue({ - name: "CREATIVEHUB_API_KEY", - withDecryption: true, - }) + const creativehubApiKey = await getParameterValue({ + name: "CREATIVEHUB_API_KEY", + withDecryption: true, + }) - if (!creativehubApiKey) { - throw new Error("No Creativehub API key") - } + if (!creativehubApiKey) { + throw new Error("No Creativehub API key") + } - const url = `${process.env.CREATIVEHUB_API_URL}/api/v1/orders/embryonic` - - const requestBody = { - ExternalReference: "", - FirstName: customer_details.name, - LastName: "", - Email: customer_details.email, - MessageToLab: "", - ShippingAddress: { - FirstName: customer_details.name, - LastName: "", - Line1: shipping_details.address.line1, - Line2: shipping_details.address.line2, - Town: shipping_details.address.city, - County: shipping_details.address.state, - PostCode: shipping_details.address.postal_code, - CountryId: getCountryIdFromCountryCode( - shipping_details.address.country - ), - CountryCode: shipping_details.address.country, - CountryName: getCountryNameFromCountryCode( - shipping_details.address.country - ), - PhoneNumber: null, + const response = await fetch( + `${process.env.CREATIVEHUB_API_URL}/api/v1/orders/embryonic`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `ApiKey ${creativehubApiKey}`, }, - OrderItems: line_items.data.map(item => ({ - ProductId: 36026, - PrintOptionId: 4175, - Quantity: item.quantity, - ExternalReference: item.price?.product.metadata.sku, - ExternalSku: "", - })), + body: JSON.stringify({ + ExternalReference: id, + FirstName: customer_details?.name, + LastName: "TEST", + Email: customer_details?.email, + MessageToLab: null, + ShippingAddress: { + FirstName: customer_details?.name, + LastName: "TEST", + Line1: shipping_details?.address?.line1, + Line2: shipping_details?.address?.line2, + Town: shipping_details?.address?.city, + County: shipping_details?.address?.state, + PostCode: shipping_details?.address?.postal_code, + CountryId: getCountryIdFromCountryCode( + shipping_details?.address?.country || "" + ), + CountryCode: shipping_details?.address?.country, + CountryName: getCountryNameFromCountryCode( + shipping_details?.address?.country || "" + ), + PhoneNumber: null, + }, + OrderItems: line_items?.data.map(item => ({ + ProductId: 36026, + PrintOptionId: 4175, + Quantity: item.quantity, + ExternalReference: item.price?.product.metadata.sku, + ExternalSku: "", + })), + }), } + ) - console.log(JSON.stringify(requestBody)) + const data = (await response.json()) as CreateEmbryonicOrderResponse + console.log("CreateEmbryonicOrderResponse: ", JSON.stringify(data)) + + if (!response.ok) { + console.error(response) + throw new Error("Failed to create order with theprintspace") + } - const options = { + return { orderId: data.Id, deliveryOptions: data.DeliveryOptions } +} + +export const createConfirmedOrder = async ({ + orderId, + deliveryOptions, +}: { + orderId: number + deliveryOptions: CreateEmbryonicOrderResponse["DeliveryOptions"] +}) => { + const creativehubApiKey = await getParameterValue({ + name: "CREATIVEHUB_API_KEY", + withDecryption: true, + }) + + if (!creativehubApiKey) { + throw new Error("No Creativehub API key") + } + + const deliveryOption = deliveryOptions.reduce((min, option) => + option.DeliveryChargeExcludingSalesTax < + min.DeliveryChargeExcludingSalesTax + ? option + : min + ) + + const response = await fetch( + `${process.env.CREATIVEHUB_API_URL}/api/v1/orders/confirmed`, + { method: "POST", headers: { "Content-Type": "application/json", Authorization: `ApiKey ${creativehubApiKey}`, }, - body: JSON.stringify(requestBody), + body: JSON.stringify({ + OrderId: orderId, + DeliveryOptionId: deliveryOption.Id, + }), } + ) - const response = await fetch(url, options) + const data = (await response.json()) as CreateConfirmedOrderResponse + console.log("CreateConfirmedOrderResponse: ", JSON.stringify(data)) - if (!response.ok) { - console.error(response.body) - console.log(response.status, response.statusText) - throw new Error("Failed to create order with theprintspace") - } - } catch (error) { - console.error(error) - throw error + if (!response.ok) { + console.error(response) + throw new Error("Failed to create order with theprintspace") } } From 017366a7b0e23865639ef721d146ce5dd941b235 Mon Sep 17 00:00:00 2001 From: Andrea Diotallevi Date: Thu, 22 Aug 2024 20:24:55 +0300 Subject: [PATCH 5/7] feat: create types file for theprintspace --- serverless/src/types/theprintspace.ts | 160 ++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 serverless/src/types/theprintspace.ts diff --git a/serverless/src/types/theprintspace.ts b/serverless/src/types/theprintspace.ts new file mode 100644 index 0000000..975c822 --- /dev/null +++ b/serverless/src/types/theprintspace.ts @@ -0,0 +1,160 @@ +type ShippingAddress = { + FirstName: string + LastName: string + Line1: string + Line2: string + Town: string + County: string + PostCode: string + CountryId: number + CountryCode: string + CountryName: string + PhoneNumber: string +} + +type Product = { + Id: number + FileName: string + DisplayName: string + Description: string + StoragePrefix: string + GUID: string + HDPI: number + VDPI: number + Width: number + Height: number + DateTaken: string + UserFirstName: string + UserLastName: string + UserDefaultArtistName: string + ArtistName: string + Paper: string + PrintType: string + HasFramedOptions: boolean + PrintOptions: PrintOption[] + DescriptionHTML: string + DateTakenString: string + ThumbnailUrl: string +} + +type PrintOption = { + Id: number + Price: number + CostPerItem: number + ShortSideInches: number + LongSideInches: number + ShortSideMM: number + LongSideMM: number + BorderTopMM: number + BorderLeftMM: number + BorderRightMM: number + BorderBottomMM: number + WMBorderTopMM: number + WMBorderLeftMM: number + WMBorderRightMM: number + WMBorderBottomMM: number + WMColour: string + IsAvailable: boolean + SellAsEdition: boolean + EditionsLimit: number + LastUpdated: string + HasFrame: boolean + HasMounting: boolean + HasCanvas: boolean + FrameTypeDescription: string + SubstrateDescription: string + FrameDescription: string + FrameWidthMM: number + WidthMM: number + HeightMM: number + EditionsSold: number + CurrencyCode: string + PreviewFileNameStandard: string + PreviewFileNameCloseUp: string + VerticalBorderMM: number + HorizontalBorderMM: number + LongBorderMM: number + ShortBorderMM: number + VerticalWMBorderMM: number + HorizontalWMBorderMM: number + LongMountMM: number + ShortMountMM: number + TotalWidthMM: number + TotalHeightMM: number + TotalLongSideMM: number + TotalShortSideMM: number + TotalLongSideInches: number + TotalShortSideInches: number + AdditionalPricing: number + CustomFinishingDetails: string + ExternalSku: string + DoNotPrint: boolean + Description: string + ShortDescription: string + FullDescription: string + VariantDescription: string + Dimensions: { + Item1: number + Item2: number + Item3: number + Item4: number + } +} + +type OrderItem = { + Id: number + ProductId: number + PrintOptionId: number + Quantity: number + ExternalReference: string + ExternalSku: string + Product: Product + PrintOption: PrintOption +} + +type DeliveryOption = { + Id: number + BranchId: number + BranchName: string + Method: string + DeliveryTime: string + DeliveryChargeExcludingSalesTax: number + DeliveryChargeSalesTax: number + EstimatedDeliveryDateFrom: string + EstimatedDeliveryDateTo: string +} + +export type CreateEmbryonicOrderResponse = { + Id: number + ExternalReference: string + FirstName: string + LastName: string + Email: string + MessageToLab: string + ShippingAddress: ShippingAddress + OrderItems: OrderItem[] + OrderState: string + DateCreated: string + DateLastEdited: string + PrintCostExcludingSalesTax: number + PrintCostSalesTax: number + TotalExcludingSalesTax: number + TotalSalesTax: number + IsPaid: boolean + DateCreatedString: string + DateLastEditedString: string + DeliveryOptions: DeliveryOption[] +} + +export type CreateConfirmedOrderResponse = { + Id: number + ExternalReference: string + FirstName: string + LastName: string + Email: string + MessageToLab: string + ShippingAddress: ShippingAddress + OrderItems: OrderItem[] + OrderState: string + DateCreated: string +} From bddbc41d133efea0d286647393be731fc758b9b6 Mon Sep 17 00:00:00 2001 From: Andrea Diotallevi Date: Thu, 22 Aug 2024 20:25:47 +0300 Subject: [PATCH 6/7] fix: show currency the customer has paid with --- gatsby/src/pages/shop/checkout/success.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/gatsby/src/pages/shop/checkout/success.tsx b/gatsby/src/pages/shop/checkout/success.tsx index b64b788..411d9b7 100644 --- a/gatsby/src/pages/shop/checkout/success.tsx +++ b/gatsby/src/pages/shop/checkout/success.tsx @@ -34,6 +34,7 @@ const Success = ({ if (!sessionId) return const session = await retrieveCheckoutSession({ sessionId }) + console.log(session) if (!session) return @@ -44,6 +45,21 @@ const Success = ({ fetchSession() }, [sessionId]) + type Currency = "eur" | "gbp" | "usd" + + const currencyToSymbol: Record = { + eur: "€", + gbp: "£", + usd: "$", + chf: "₣", // Switzerland + nok: "kr", // Norway + dkk: "kr", // Denmark + sek: "kr", // Sweden + } + + const currencySymbol = + currencyToSymbol[(session?.currency as Currency) || "gbp"] + return ( {session ? ( @@ -111,19 +127,19 @@ const Success = ({ ))}

Payment summary

- Subtotal: £ + Subtotal: {currencySymbol} {((session.amount_subtotal || 0) / 100).toFixed( 2, )}

Shipping fee: Free

- Discounts: £ + Discounts: {currencySymbol} {( (session.total_details?.amount_discount || 0) / 100 ).toFixed(2)}

- Total: £ + Total: {currencySymbol} {((session.amount_total || 0) / 100).toFixed(2)}