Guest User

Untitled

a guest
Sep 25th, 2025
31
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
TypeScript 11.61 KB | Source Code | 0 0
  1. import { AbstractPaymentProvider, MedusaError } from "@medusajs/framework/utils"
  2. import { Logger } from "@medusajs/medusa"
  3. import {
  4.   SquareClient as Client,
  5.   SquareEnvironment as Environment,
  6.   Square
  7. } from "square"
  8. import {
  9.   AuthorizePaymentInput,
  10.   AuthorizePaymentOutput,
  11.   CancelPaymentInput,
  12.   CancelPaymentOutput,
  13.   CapturePaymentInput,
  14.   CapturePaymentOutput,
  15.   DeletePaymentInput,
  16.   DeletePaymentOutput,
  17.   GetPaymentStatusInput,
  18.   GetPaymentStatusOutput,
  19.   InitiatePaymentInput,
  20.   InitiatePaymentOutput,
  21.   PaymentSessionStatus,
  22.   ProviderWebhookPayload,
  23.   RefundPaymentInput,
  24.   RefundPaymentOutput,
  25.   RetrievePaymentInput,
  26.   RetrievePaymentOutput,
  27.   UpdatePaymentInput,
  28.   UpdatePaymentOutput,
  29.   WebhookActionResult
  30. } from "@medusajs/framework/types"
  31.  
  32. export type Options = {
  33.   access_token: string
  34.   application_id?: string
  35.   location_id?: string
  36.   environment?: 'sandbox' | 'production'
  37.   webhook_secret?: string
  38.   capture_method?: 'automatic' | 'manual'
  39. }
  40.  
  41. type InjectedDependencies = {
  42.   logger: Logger
  43. }
  44.  
  45. export class SquarePaymentProvider extends AbstractPaymentProvider<Options> {
  46.   static identifier = "square-payment-provider"
  47.  
  48.   protected logger_: Logger
  49.   protected options_: Options
  50.   protected squareClient_: Client
  51.  
  52.   constructor(container: InjectedDependencies, options: Options) {
  53.     super(container, options)
  54.  
  55.     this.logger_ = container.logger
  56.     this.options_ = options
  57.  
  58.     this.squareClient_ = new Client({
  59.       token: options.access_token,
  60.       environment: options.environment === 'production' ? Environment.Production : Environment.Sandbox,
  61.     })
  62.   }
  63.  
  64.   static validateOptions(options: Options) {
  65.     if (!options.access_token) {
  66.       throw new MedusaError(
  67.         MedusaError.Types.INVALID_DATA,
  68.         "Square access token is required in the provider's options."
  69.       )
  70.     }
  71.   }
  72.  
  73.   async getPaymentStatus(input: GetPaymentStatusInput): Promise<GetPaymentStatusOutput> {
  74.     const paymentId = input.data?.id as string
  75.  
  76.     try {
  77.       const { payment } = await this.squareClient_.payments.get({
  78.         paymentId
  79.       })
  80.  
  81.       if (payment) {
  82.         return {
  83.           status: this.mapSquareStatusToMedusa(payment.status || 'PENDING'),
  84.         }
  85.       } else {
  86.         return {
  87.           status: this.mapSquareStatusToMedusa('PENDING')
  88.         }
  89.       }
  90.     } catch (error) {
  91.       this.logger_.error(`Failed to get Square payment status: ${error}`)
  92.       return {
  93.         status: this.mapSquareStatusToMedusa('PENDING')
  94.       }
  95.     }
  96.   }
  97.  
  98.   async initiatePayment(input: InitiatePaymentInput): Promise<InitiatePaymentOutput> {
  99.     try {
  100.       const { context = {}, data = {}, amount, currency_code } = input
  101.  
  102.       const paymentIntent = {
  103.         sourceId: data.source_id || data.nonce,
  104.         idempotencyKey: context.idempotency_key || `auth_${Date.now()}`,
  105.         amountMoney: {
  106.           amount: BigInt(amount.toString()),
  107.           currency: currency_code.toUpperCase() as Square.Currency
  108.         },
  109.         orderId: data?.orderId,
  110.         locationId: data?.locationId || this.options_.location_id,
  111.         customerId: context?.customer?.id,
  112.         buyerEmailAddress: context?.customer?.email,
  113.         buyerPhoneNumber: context?.customer?.phone,
  114.         referenceId: context?.idempotency_key,
  115.         captureMethod: this.options_.capture_method || 'automatic',
  116.         createdAt: new Date(),
  117.         status: 'PENDING'
  118.       } as Square.CreatePaymentRequest
  119.  
  120.       this.logger_.info(`Square payment initiated: ${paymentIntent.idempotencyKey} for amount: ${amount} ${currency_code}`)
  121.  
  122.       const { payment } = await this.squareClient_.payments.create(paymentIntent)
  123.  
  124.       if (!payment) {
  125.         throw new MedusaError(
  126.           MedusaError.Types.INVALID_DATA,
  127.           "Square payment initiation failed"
  128.         )
  129.       }
  130.  
  131.       return {
  132.         id: payment.id!,
  133.         data: this.mapSquarePaymentToMedusa(payment, paymentIntent.idempotencyKey),
  134.       }
  135.     } catch (error) {
  136.       this.logger_.error(`Square payment initiation failed: ${error}`)
  137.       throw error
  138.     }
  139.   }
  140.  
  141.   async authorizePayment(
  142.     input: AuthorizePaymentInput
  143.   ): Promise<AuthorizePaymentOutput> {
  144.     try {
  145.       if (!input.data || !input.data.id || !input.data.idempotencyKey) {
  146.         throw new MedusaError(
  147.           MedusaError.Types.INVALID_DATA,
  148.           "Payment data with valid id and idempotencyKey is required for authorization"
  149.         )
  150.       }
  151.  
  152.       const { payment } = await this.squareClient_.payments.update({
  153.         paymentId: input.data.id as string,
  154.         idempotencyKey: input.data.idempotencyKey as string,
  155.         payment: {
  156.           status: 'APPROVED'
  157.         }
  158.       })
  159.  
  160.       return {
  161.         status: this.mapSquareStatusToMedusa(payment?.status || 'PENDING'),
  162.         data: this.mapSquarePaymentToMedusa(payment!, input.data.idempotencyKey as string),
  163.       }
  164.     } catch (error) {
  165.       this.logger_.error(`Square payment authorization failed: ${error}`)
  166.       throw error
  167.     }
  168.   }
  169.  
  170.   async capturePayment(input: CapturePaymentInput): Promise<CapturePaymentOutput> {
  171.     try {
  172.       if (!input.data || !input.data.id) {
  173.         throw new MedusaError(
  174.           MedusaError.Types.INVALID_DATA,
  175.           "Payment data with valid id is required for capture"
  176.         )
  177.       }
  178.  
  179.       const paymentId = input.data.id as string
  180.       this.logger_.info(`Capturing Square payment: ${paymentId}`)
  181.  
  182.       const { payment } = await this.squareClient_.payments.complete({
  183.         paymentId: paymentId,
  184.       })
  185.  
  186.       if (payment) {
  187.         return {
  188.           data: this.mapSquarePaymentToMedusa(payment, input.data.idempotencyKey as string),
  189.         }
  190.       } else {
  191.         throw new Error("Square payment capture failed")
  192.       }
  193.     } catch (error) {
  194.       this.logger_.error(`Square payment capture failed: ${error}`)
  195.       throw error
  196.     }
  197.   }
  198.  
  199.  
  200.   async cancelPayment(input: CancelPaymentInput): Promise<CancelPaymentOutput> {
  201.     try {
  202.       if (!input.data || !input.data.id) {
  203.         throw new MedusaError(
  204.           MedusaError.Types.INVALID_DATA,
  205.           "Payment data with valid id is required for cancellation"
  206.         )
  207.       }
  208.  
  209.       const paymentId = input.data.id as string
  210.  
  211.       this.logger_.info(`Canceling Square payment: ${paymentId}`)
  212.  
  213.       const { payment } = await this.squareClient_.payments.cancel({
  214.         paymentId
  215.       })
  216.  
  217.       if (payment) {
  218.         return {
  219.           data: this.mapSquarePaymentToMedusa(payment, input.data.idempotencyKey as string),
  220.         }
  221.       } else {
  222.         throw new Error("Square payment cancellation failed")
  223.       }
  224.     } catch (error) {
  225.       this.logger_.error(`Square payment cancellation failed: ${error}`)
  226.       throw error
  227.     }
  228.   }
  229.  
  230.   async deletePayment(input: DeletePaymentInput): Promise<DeletePaymentOutput> {
  231.     // Square doesn't support deleting payments, only canceling
  232.     return this.cancelPayment(input)
  233.   }
  234.  
  235.   async retrievePayment(input: RetrievePaymentInput): Promise<RetrievePaymentOutput> {
  236.     try {
  237.       if (!input.data || !input.data.id) {
  238.         throw new MedusaError(
  239.           MedusaError.Types.INVALID_DATA,
  240.           "Payment data with valid id is required for retrieval"
  241.         )
  242.       }
  243.  
  244.       const paymentId = input.data.id as string
  245.  
  246.       const { payment } = await this.squareClient_.payments.get({
  247.         paymentId
  248.       })
  249.  
  250.       if (payment) {
  251.         return {
  252.           data: this.mapSquarePaymentToMedusa(payment, input.data.idempotencyKey as string)
  253.         }
  254.       } else {
  255.         throw new Error("Square payment not found")
  256.       }
  257.     } catch (error) {
  258.       this.logger_.error(`Square payment retrieval failed: ${error}`)
  259.       throw error
  260.     }
  261.   }
  262.  
  263.   async updatePayment(input: UpdatePaymentInput): Promise<UpdatePaymentOutput> {
  264.     try {
  265.       if (!input.data || !input.data.id || !input.data.idempotencyKey) {
  266.         throw new MedusaError(
  267.           MedusaError.Types.INVALID_DATA,
  268.           "Payment data with valid id and idempotencyKey is required for update"
  269.         )
  270.       }
  271.  
  272.       const paymentId = input.data.id as string
  273.  
  274.       const { payment } = await this.squareClient_.payments.update({
  275.         paymentId,
  276.         idempotencyKey: input.data.idempotencyKey as string,
  277.         payment: input.data
  278.       })
  279.  
  280.       if (payment) {
  281.         return {
  282.           data: this.mapSquarePaymentToMedusa(payment, input.data.idempotencyKey as string),
  283.         }
  284.       } else {
  285.         throw new Error("Square payment update failed")
  286.       }
  287.     } catch (error) {
  288.       this.logger_.error(`Square payment update failed: ${error}`)
  289.       throw error
  290.     }
  291.   }
  292.  
  293.   async refundPayment(
  294.     input: RefundPaymentInput
  295.   ): Promise<RefundPaymentOutput> {
  296.    
  297.     try {
  298.       if (!input.data || !input.data.id || !input.data.idempotencyKey) {
  299.         throw new Error("Square payment ID is required for refund")
  300.       }
  301.  
  302.       const paymentId = input.data.id as string
  303.  
  304.       this.logger_.info(`Creating Square refund for payment: ${paymentId}`)
  305.  
  306.       const { refund } = await this.squareClient_.refunds.refundPayment({
  307.         idempotencyKey: input.data.idempotencyKey as string,
  308.         paymentId: paymentId,
  309.         amountMoney: {
  310.           amount: BigInt(input.amount.toString()),
  311.           currency: input.data.currency as Square.Currency
  312.         },
  313.         reason: 'Refund requested'
  314.       })
  315.  
  316.       if (refund) {
  317.         return {
  318.           data: {
  319.             id: refund.id,
  320.             status: refund.status,
  321.             locationId: refund.locationId,
  322.             unlinked: refund.unlinked,
  323.             amountMoney: refund.amountMoney,
  324.             appFeeMoney: refund.appFeeMoney,
  325.             destinationType: refund.destinationType,
  326.             destinationDetails: refund.destinationDetails,
  327.             processingFee: refund.processingFee,
  328.             paymentId: refund.paymentId,
  329.             orderId: refund.orderId,
  330.             reason: refund.reason,
  331.             createdAt: refund.createdAt,
  332.             updatedAt: refund.updatedAt
  333.           }
  334.         }
  335.       } else {
  336.         throw new Error("Square refund creation failed")
  337.       }
  338.     } catch (error) {
  339.       this.logger_.error(`Square refund failed: ${error}`)
  340.       throw error
  341.     }
  342.   }
  343.  
  344.   async getWebhookActionAndData(data: ProviderWebhookPayload["payload"]): Promise<WebhookActionResult> {
  345.     this.logger_.info(`Received Square webhook: ${JSON.stringify(data)}`)
  346.     throw new Error("Method not implemented.")
  347.   }
  348.  
  349.   private mapSquareStatusToMedusa(squareStatus: string): PaymentSessionStatus {
  350.     const statusMap = {
  351.       'APPROVED': 'authorized',
  352.       'COMPLETED': 'captured',
  353.       'CANCELED': 'canceled',
  354.       'FAILED': 'error',
  355.       'PENDING': 'pending',
  356.     }
  357.  
  358.     return statusMap[squareStatus] || 'pending'
  359.   }
  360.  
  361.   private mapSquarePaymentToMedusa(payment: Square.Payment, idempotencyKey?: string) {
  362.     return {
  363.       id: payment.id,
  364.       idempotencyKey: idempotencyKey || (payment as any).idempotencyKey,
  365.       amount: Number(payment.amountMoney?.amount || 0),
  366.       currency: payment.amountMoney?.currency,
  367.       status: this.mapSquareStatusToMedusa(payment.status || 'PENDING'),
  368.       refundedMoney: payment.refundedMoney,
  369.       billingAddress: payment.billingAddress,
  370.       shippingAddress: payment.shippingAddress,
  371.       buyerEmailAddress: payment.buyerEmailAddress,
  372.       customerId: payment.customerId,
  373.       referenceId: payment.referenceId,
  374.       orderId: payment.orderId,
  375.       locationId: payment.locationId,
  376.       createdAt: payment.createdAt,
  377.       updatedAt: payment.updatedAt,
  378.     }
  379.   }
  380. }
  381.  
  382. export default SquarePaymentProvider
Advertisement
Add Comment
Please, Sign In to add comment