chore(): Modules providers reorganization (#7234)

Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
Adrien de PerettiandRiqwan Thamir authored and GitHub committed 2024-05-05 10:08:07 +02:00
1 parent 2f7b53488d
commit 93f6e60c17
47 files changed
+14 -9

No files matched your search

@@ -0,0 +1,4 @@
dist
node_modules
.DS_store
yarn.lock
@@ -0,0 +1,10 @@
# @medusajs/payment-stripe
## 0.0.2
### Patch Changes
- [#6700](https://github.com/medusajs/medusa/pull/6700) [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Version all modules to allow for initial testing
- Updated dependencies [[`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`56cbf88115`](https://github.com/medusajs/medusa/commit/56cbf88115994adea7037c3f2814f0c96af3cfc0), [`36a61658f9`](https://github.com/medusajs/medusa/commit/36a61658f969a7b19c84a1e621ad1464927cafb1), [`c319edb8e0`](https://github.com/medusajs/medusa/commit/c319edb8e0ecd13d086652147667916e5abab2d8), [`0b9fcb6324`](https://github.com/medusajs/medusa/commit/0b9fcb6324eee9f2556c7e6317775fae93b12a47), [`b3d826497b`](https://github.com/medusajs/medusa/commit/b3d826497b3dae5e1b26b7924706c24fd5e87ca5), [`a86c87fe14`](https://github.com/medusajs/medusa/commit/a86c87fe1442afce9285e39255914e01012b4449), [`640eccd5dd`](https://github.com/medusajs/medusa/commit/640eccd5ddbb163e0f987ce6c772f1129c2e2632), [`8ea37d03c9`](https://github.com/medusajs/medusa/commit/8ea37d03c914a5004a3e42770668b2d1f7f8f564), [`339a946f38`](https://github.com/medusajs/medusa/commit/339a946f389033c21e05338f9dbf07d88e140533), [`9288f53327`](https://github.com/medusajs/medusa/commit/9288f53327b8ce617af92ed8d14d9459cbfeb13c), [`8dad2b51a2`](https://github.com/medusajs/medusa/commit/8dad2b51a26c4c3c14a6c95f70424c8bef2ad63e), [`a6d7070dd6`](https://github.com/medusajs/medusa/commit/a6d7070dd669c21ea19d70434d42c2f8167dc309), [`168f02f138`](https://github.com/medusajs/medusa/commit/168f02f138ad101e1013f2c8c3f8dc19de12accf), [`f5c2256286`](https://github.com/medusajs/medusa/commit/f5c22562867f412040f8bc6c55ab5de3a3735e62), [`000eb61e33`](https://github.com/medusajs/medusa/commit/000eb61e33e0302db95ee6ad1656ea9b430ed471), [`62a7bcc30c`](https://github.com/medusajs/medusa/commit/62a7bcc30cbc7b234b2b51d7858439951a84edeb), [`8f8a4f9b13`](https://github.com/medusajs/medusa/commit/8f8a4f9b1353087d98f6cc75346d43a7f49901a8), [`6500f18b9b`](https://github.com/medusajs/medusa/commit/6500f18b9b80c5c9c473489e7e740d55dca74303), [`ce39b9b66e`](https://github.com/medusajs/medusa/commit/ce39b9b66e8c277ec0691ea6d0a950003be09cc1), [`a6a4b3f01a`](https://github.com/medusajs/medusa/commit/a6a4b3f01a6d2bd97b1580c59134279a1b033a5d), [`56b0b45304`](https://github.com/medusajs/medusa/commit/56b0b4530401a6ec5aa155874d371e45bb388fe2), [`cc1b66842c`](https://github.com/medusajs/medusa/commit/cc1b66842cbb37c6eab84e2d8b74844c214f38d7), [`e85463b2a7`](https://github.com/medusajs/medusa/commit/e85463b2a717751de2e21c39a4c745449b31affe)]:
- @medusajs/utils@1.11.7
Whitespace-only changes.
@@ -0,0 +1,13 @@
module.exports = {
globals: {
"ts-jest": {
tsconfig: "tsconfig.spec.json",
isolatedModules: false,
},
},
transform: {
"^.+\\.[jt]s?$": "ts-jest",
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
}
@@ -0,0 +1,48 @@
{
"name": "@medusajs/payment-stripe",
"version": "0.0.2",
"description": "Stripe payment provider for Medusa",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/payment-stripe"
},
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"test": "jest --passWithNoTests src",
"build": "rimraf dist && tsc -p ./tsconfig.json",
"watch": "tsc --watch"
},
"devDependencies": {
"@medusajs/medusa": "^1.20.3",
"@types/stripe": "^8.0.417",
"awilix": "^8.0.1",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"rimraf": "^5.0.1",
"typescript": "^4.9.5"
},
"peerDependencies": {
"@medusajs/medusa": "^1.12.0"
},
"dependencies": {
"@medusajs/utils": "^1.11.7",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"stripe": "latest"
},
"gitHead": "81a7ff73d012fda722f6e9ef0bd9ba0232d37808",
"keywords": [
"medusa-plugin",
"medusa-plugin-payment"
]
}
@@ -0,0 +1,381 @@
import { EOL } from "os"
import Stripe from "stripe"
import {
MedusaContainer,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
ProviderWebhookPayload,
UpdatePaymentProviderSession,
WebhookActionResult,
} from "@medusajs/types"
import {
AbstractPaymentProvider,
BigNumber,
MedusaError,
PaymentActions,
isPaymentProviderError,
} from "@medusajs/utils"
import { isDefined } from "medusa-core-utils"
import { CreatePaymentProviderSession } from "@medusajs/types"
import {
ErrorCodes,
ErrorIntentStatus,
PaymentIntentOptions,
StripeCredentials,
StripeOptions,
} from "../types"
abstract class StripeBase extends AbstractPaymentProvider<StripeCredentials> {
protected readonly options_: StripeOptions
protected stripe_: Stripe
protected container_: MedusaContainer
protected constructor(container: MedusaContainer, options: StripeOptions) {
// @ts-ignore
super(...arguments)
this.container_ = container
this.options_ = options
this.stripe_ = this.init()
}
protected init() {
this.validateOptions(this.config)
return new Stripe(this.config.apiKey)
}
abstract get paymentIntentOptions(): PaymentIntentOptions
private validateOptions(options: StripeCredentials): void {
if (!isDefined(options.apiKey)) {
throw new Error("Required option `apiKey` is missing in Stripe plugin")
}
}
get options(): StripeOptions {
return this.options_
}
getPaymentIntentOptions(): PaymentIntentOptions {
const options: PaymentIntentOptions = {}
if (this?.paymentIntentOptions?.capture_method) {
options.capture_method = this.paymentIntentOptions.capture_method
}
if (this?.paymentIntentOptions?.setup_future_usage) {
options.setup_future_usage = this.paymentIntentOptions.setup_future_usage
}
if (this?.paymentIntentOptions?.payment_method_types) {
options.payment_method_types =
this.paymentIntentOptions.payment_method_types
}
return options
}
async getPaymentStatus(
paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> {
const id = paymentSessionData.id as string
const paymentIntent = await this.stripe_.paymentIntents.retrieve(id)
switch (paymentIntent.status) {
case "requires_payment_method":
case "requires_confirmation":
case "processing":
return PaymentSessionStatus.PENDING
case "requires_action":
return PaymentSessionStatus.REQUIRES_MORE
case "canceled":
return PaymentSessionStatus.CANCELED
case "requires_capture":
case "succeeded":
return PaymentSessionStatus.AUTHORIZED
default:
return PaymentSessionStatus.PENDING
}
}
async initiatePayment(
input: CreatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
const intentRequestData = this.getPaymentIntentOptions()
const { email, extra, resource_id, customer } = input.context
const { currency_code, amount } = input
const description = (extra?.payment_description ??
this.options_?.payment_description) as string
const intentRequest: Stripe.PaymentIntentCreateParams = {
description,
amount: Math.round(new BigNumber(amount).numeric),
currency: currency_code,
metadata: { resource_id: resource_id ?? "Medusa Payment" },
capture_method: this.options_.capture ? "automatic" : "manual",
...intentRequestData,
}
if (this.options_?.automatic_payment_methods) {
intentRequest.automatic_payment_methods = { enabled: true }
}
if (customer?.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id as string
} else {
let stripeCustomer
try {
stripeCustomer = await this.stripe_.customers.create({
email,
})
} catch (e) {
return this.buildError(
"An error occurred in initiatePayment when creating a Stripe customer",
e
)
}
intentRequest.customer = stripeCustomer.id
}
let sessionData
try {
sessionData = (await this.stripe_.paymentIntents.create(
intentRequest
)) as unknown as Record<string, unknown>
} catch (e) {
return this.buildError(
"An error occurred in InitiatePayment during the creation of the stripe payment intent",
e
)
}
return {
data: sessionData,
// TODO: REVISIT
// update_requests: customer?.metadata?.stripe_id
// ? undefined
// : {
// customer_metadata: {
// stripe_id: intentRequest.customer,
// },
// },
}
}
async authorizePayment(
paymentSessionData: Record<string, unknown>,
context: Record<string, unknown>
): Promise<
| PaymentProviderError
| {
status: PaymentSessionStatus
data: PaymentProviderSessionResponse["data"]
}
> {
const status = await this.getPaymentStatus(paymentSessionData)
return { data: paymentSessionData, status }
}
async cancelPayment(
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
try {
const id = paymentSessionData.id as string
return (await this.stripe_.paymentIntents.cancel(
id
)) as unknown as PaymentProviderSessionResponse["data"]
} catch (error) {
if (error.payment_intent?.status === ErrorIntentStatus.CANCELED) {
return error.payment_intent
}
return this.buildError("An error occurred in cancelPayment", error)
}
}
async capturePayment(
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
const id = paymentSessionData.id as string
try {
const intent = await this.stripe_.paymentIntents.capture(id)
return intent as unknown as PaymentProviderSessionResponse["data"]
} catch (error) {
if (error.code === ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) {
if (error.payment_intent?.status === ErrorIntentStatus.SUCCEEDED) {
return error.payment_intent
}
}
return this.buildError("An error occurred in capturePayment", error)
}
}
async deletePayment(
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
return await this.cancelPayment(paymentSessionData)
}
async refundPayment(
paymentSessionData: Record<string, unknown>,
refundAmount: number
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
const id = paymentSessionData.id as string
try {
await this.stripe_.refunds.create({
amount: Math.round(refundAmount),
payment_intent: id as string,
})
} catch (e) {
return this.buildError("An error occurred in refundPayment", e)
}
return paymentSessionData
}
async retrievePayment(
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | PaymentProviderSessionResponse["data"]> {
try {
const id = paymentSessionData.id as string
const intent = await this.stripe_.paymentIntents.retrieve(id)
return intent as unknown as PaymentProviderSessionResponse["data"]
} catch (e) {
return this.buildError("An error occurred in retrievePayment", e)
}
}
async updatePayment(
input: UpdatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
const { context, data, amount } = input
const amountNumeric = Math.round(new BigNumber(amount).numeric)
const stripeId = context.customer?.metadata?.stripe_id
if (stripeId !== data.customer) {
const result = await this.initiatePayment(input)
if (isPaymentProviderError(result)) {
return this.buildError(
"An error occurred in updatePayment during the initiate of the new payment for the new customer",
result
)
}
return result
} else {
if (amount && data.amount === amountNumeric) {
return { data }
}
try {
const id = data.id as string
const sessionData = (await this.stripe_.paymentIntents.update(id, {
amount: amountNumeric,
})) as unknown as PaymentProviderSessionResponse["data"]
return { data: sessionData }
} catch (e) {
return this.buildError("An error occurred in updatePayment", e)
}
}
}
async updatePaymentData(sessionId: string, data: Record<string, unknown>) {
try {
// Prevent from updating the amount from here as it should go through
// the updatePayment method to perform the correct logic
if (data.amount) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot update amount, use updatePayment instead"
)
}
return (await this.stripe_.paymentIntents.update(sessionId, {
...data,
})) as unknown as PaymentProviderSessionResponse["data"]
} catch (e) {
return this.buildError("An error occurred in updatePaymentData", e)
}
}
async getWebhookActionAndData(
webhookData: ProviderWebhookPayload["payload"]
): Promise<WebhookActionResult> {
const event = this.constructWebhookEvent(webhookData)
const intent = event.data.object as Stripe.PaymentIntent
switch (event.type) {
case "payment_intent.amount_capturable_updated":
return {
action: PaymentActions.AUTHORIZED,
data: {
resource_id: intent.metadata.resource_id,
amount: intent.amount_capturable, // NOTE: revisit when implementing multicapture
},
}
case "payment_intent.succeeded":
return {
action: PaymentActions.SUCCESSFUL,
data: {
resource_id: intent.metadata.resource_id,
amount: intent.amount_received,
},
}
case "payment_intent.payment_failed":
return {
action: PaymentActions.FAILED,
data: {
resource_id: intent.metadata.resource_id,
amount: intent.amount,
},
}
default:
return { action: PaymentActions.NOT_SUPPORTED }
}
}
/**
* Constructs Stripe Webhook event
* @param {object} data - the data of the webhook request: req.body
* ensures integrity of the webhook event
* @return {object} Stripe Webhook event
*/
constructWebhookEvent(data: ProviderWebhookPayload["payload"]): Stripe.Event {
const signature = data.headers["stripe-signature"] as string
return this.stripe_.webhooks.constructEvent(
data.rawData as string | Buffer,
signature,
this.config.webhookSecret
)
}
protected buildError(
message: string,
error: Stripe.StripeRawError | PaymentProviderError | Error
): PaymentProviderError {
return {
error: message,
code: "code" in error ? error.code : "unknown",
detail: isPaymentProviderError(error)
? `${error.error}${EOL}${error.detail ?? ""}`
: "detail" in error
? error.detail
: error.message ?? "",
}
}
}
export default StripeBase
@@ -0,0 +1,24 @@
import { ModuleProviderExports } from "@medusajs/types"
import {
StripeBancontactService,
StripeBlikService,
StripeGiropayService,
StripeIdealService,
StripeProviderService,
StripePrzelewy24Service,
} from "./services"
const services = [
StripeBancontactService,
StripeBlikService,
StripeGiropayService,
StripeIdealService,
StripeProviderService,
StripePrzelewy24Service,
]
const providerExport: ModuleProviderExports = {
services,
}
export default providerExport
@@ -0,0 +1,7 @@
export { default as StripeBancontactService } from "./stripe-bancontact"
export { default as StripeBlikService } from "./stripe-blik"
export { default as StripeGiropayService } from "./stripe-giropay"
export { default as StripeIdealService } from "./stripe-ideal"
export { default as StripeProviderService } from "./stripe-provider"
export { default as StripePrzelewy24Service } from "./stripe-przelewy24"
@@ -0,0 +1,19 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class BancontactProviderService extends StripeBase {
static PROVIDER = PaymentProviderKeys.BAN_CONTACT
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["bancontact"],
capture_method: "automatic",
}
}
}
export default BancontactProviderService
@@ -0,0 +1,19 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class BlikProviderService extends StripeBase {
static PROVIDER = PaymentProviderKeys.BLIK
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["blik"],
capture_method: "automatic",
}
}
}
export default BlikProviderService
@@ -0,0 +1,19 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class GiropayProviderService extends StripeBase {
static PROVIDER = PaymentProviderKeys.GIROPAY
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["giropay"],
capture_method: "automatic",
}
}
}
export default GiropayProviderService
@@ -0,0 +1,19 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class IdealProviderService extends StripeBase {
static PROVIDER = PaymentProviderKeys.IDEAL
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["ideal"],
capture_method: "automatic",
}
}
}
export default IdealProviderService
@@ -0,0 +1,16 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class StripeProviderService extends StripeBase {
static PROVIDER = PaymentProviderKeys.STRIPE
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {}
}
}
export default StripeProviderService
@@ -0,0 +1,19 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class Przelewy24ProviderService extends StripeBase {
static PROVIDER = PaymentProviderKeys.PRZELEWY_24
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["p24"],
capture_method: "automatic",
}
}
}
export default Przelewy24ProviderService
@@ -0,0 +1,44 @@
export interface StripeCredentials {
apiKey: string
webhookSecret: string
}
export interface StripeOptions {
credentials: Record<string, StripeCredentials>
/**
* Use this flag to capture payment immediately (default is false)
*/
capture?: boolean
/**
* set `automatic_payment_methods` to `{ enabled: true }`
*/
automatic_payment_methods?: boolean
/**
* Set a default description on the intent if the context does not provide one
*/
payment_description?: string
}
export interface PaymentIntentOptions {
capture_method?: "automatic" | "manual"
setup_future_usage?: "on_session" | "off_session"
payment_method_types?: string[]
}
export const ErrorCodes = {
PAYMENT_INTENT_UNEXPECTED_STATE: "payment_intent_unexpected_state",
}
export const ErrorIntentStatus = {
SUCCEEDED: "succeeded",
CANCELED: "canceled",
}
export const PaymentProviderKeys = {
STRIPE: "stripe",
BAN_CONTACT: "stripe-bancontact",
BLIK: "stripe-blik",
GIROPAY: "stripe-giropay",
IDEAL: "stripe-ideal",
PRZELEWY_24: "stripe-przelewy24",
}
@@ -0,0 +1,36 @@
{
"compilerOptions": {
"lib": [
"es5",
"es6",
"es2019"
],
"target": "es5",
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true, // to use ES5 specific tooling
"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
},
"include": ["src"],
"exclude": [
"dist",
"build",
"src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules",
".eslintrc.js"
]
}
@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}