Skip to content
Merged
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
41 changes: 34 additions & 7 deletions apps/medusa-be/medusa-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
} from "@medusajs/framework/utils"
import { buildProductFacetDocument } from "./src/modules/meilisearch/facets/product-facets"
import { buildPaykitPaymentProviders } from "./src/modules/payment-paykit/medusa-config"
import {
QR_PAYMENT_MODULE,
QR_PAYMENT_PROVIDER_ID,
} from "./src/modules/payment-qr/constants"

loadEnv(process.env.NODE_ENV || "development", process.cwd())

Expand All @@ -24,11 +28,27 @@ const MEILISEARCH_TYPO_TOLERANCE_SETTINGS = {
disableOnNumbers: false,
}

const FEATURE_PPL_ENABLED = process.env.FEATURE_PPL_ENABLED === "1"
const FEATURE_PACKETA_ENABLED = process.env.FEATURE_PACKETA_ENABLED === "1"
const FEATURE_PAYLOAD_ENABLED = process.env.FEATURE_PAYLOAD_ENABLED === "1"
const FEATURE_FLAG_ENABLED_VALUE = "1"

const FEATURE_PPL_ENABLED =
process.env.FEATURE_PPL_ENABLED === FEATURE_FLAG_ENABLED_VALUE
const FEATURE_PACKETA_ENABLED =
process.env.FEATURE_PACKETA_ENABLED === FEATURE_FLAG_ENABLED_VALUE
const FEATURE_PAYLOAD_ENABLED =
process.env.FEATURE_PAYLOAD_ENABLED === FEATURE_FLAG_ENABLED_VALUE
const FEATURE_PAYMENT_QR_ENABLED =
process.env.FEATURE_PAYMENT_QR_ENABLED === FEATURE_FLAG_ENABLED_VALUE

Comment thread
tomas-cm1 marked this conversation as resolved.
const PAYKIT_PAYMENT_PROVIDERS = buildPaykitPaymentProviders()
const QR_PAYMENT_PROVIDER = {
resolve: "./src/modules/payment-qr/services/manual",
id: QR_PAYMENT_PROVIDER_ID,
options: {},
}
const PAYMENT_PROVIDERS = [
...(FEATURE_PAYMENT_QR_ENABLED ? [QR_PAYMENT_PROVIDER] : []),
...PAYKIT_PAYMENT_PROVIDERS,
]

const NOTIFICATION_PROVIDER = process.env.NOTIFICATION_PROVIDER ?? "resend"
const RESEND_API_KEY = process.env.RESEND_API_KEY
Expand Down Expand Up @@ -314,9 +334,13 @@ module.exports = defineConfig({
{
resolve: "./src/modules/order-receipt",
},
{
resolve: "./src/modules/qr-payment",
},
...(FEATURE_PAYMENT_QR_ENABLED
? [
{
resolve: "./src/modules/payment-qr",
},
]
: []),
{
resolve: "@medusajs/event-bus-redis",
key: Modules.EVENT_BUS,
Expand Down Expand Up @@ -377,8 +401,11 @@ module.exports = defineConfig({
},
{
resolve: "@medusajs/medusa/payment",
dependencies: [
...(FEATURE_PAYMENT_QR_ENABLED ? [QR_PAYMENT_MODULE] : []),
],
options: {
providers: PAYKIT_PAYMENT_PROVIDERS,
providers: PAYMENT_PROVIDERS,
},
},
// PPL Client Module - config stored in DB, managed via Settings → PPL
Expand Down
1 change: 1 addition & 0 deletions apps/medusa-be/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"seedDevData": "medusa exec ./src/scripts/seed-dev-data.ts",
"seedN1": "medusa exec ./src/scripts/seed-n1.ts",
"seedPaykit": "medusa exec ./src/scripts/seed-paykit.ts",
"seedQrPayment": "medusa exec ./src/scripts/seed-qr-payment.ts",
"seedOrderBusinessStatusDemo": "medusa exec ./src/scripts/seed-order-business-status-demo.ts",
"seedInitialData": "medusa exec ./src/scripts/seed.ts",
"seedInitialDataTs": "medusa exec ./src/scripts/seed.ts",
Expand Down
4 changes: 2 additions & 2 deletions apps/medusa-be/src/api/admin/qr-payment-config/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { MedusaError } from "@medusajs/framework/utils"
import {
QR_PAYMENT_MODULE,
type QrPaymentModuleService,
} from "../../../modules/qr-payment"
} from "../../../modules/payment-qr"
import type {
QrPaymentConfigDTO,
QrPaymentConfigResponse,
} from "../../../modules/qr-payment/types"
} from "../../../modules/payment-qr/types"
import type { PostAdminQrPaymentConfigSchemaType } from "./validators"

const toConfigResponse = (
Expand Down
24 changes: 23 additions & 1 deletion apps/medusa-be/src/modules/order-receipt/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ export type OrderReceiptAddress = {

export type OrderReceiptLineItem = {
detail?: {
quantity?: OrderReceiptMoney
raw_quantity?: OrderReceiptMoney
raw_unit_price?: OrderReceiptMoney
title?: string | null
unit_price?: OrderReceiptMoney
} | null
raw_quantity?: OrderReceiptMoney
raw_unit_price?: OrderReceiptMoney
subtotal?: OrderReceiptMoney
tax_total?: OrderReceiptMoney
Expand All @@ -35,6 +38,7 @@ export type OrderReceiptLineItem = {
}

export type OrderReceiptPayment = {
data?: Record<string, unknown> | null
provider_id?: string | null
}

Expand Down Expand Up @@ -262,9 +266,27 @@ export function getItemUnitPrice(item: OrderReceiptLineItem) {
)
}

export function getItemQuantity(item: OrderReceiptLineItem) {
const quantity = toNumber(
item.quantity ??
item.detail?.quantity ??
item.raw_quantity ??
item.detail?.raw_quantity
)

return quantity > 0 ? quantity : 1
Comment thread
tomas-cm1 marked this conversation as resolved.
}

export function getItemSubtotal(item: OrderReceiptLineItem) {
const quantity = getItemQuantity(item)
const unitPrice = getItemUnitPrice(item)
const unitPriceSubtotal = quantity * unitPrice
const subtotal = toNumber(item.subtotal)
if (subtotal > 0) {
if (quantity > 1 && subtotal === unitPrice) {
return unitPriceSubtotal
}
Comment thread
tomas-cm1 marked this conversation as resolved.

return subtotal
}

Expand All @@ -274,7 +296,7 @@ export function getItemSubtotal(item: OrderReceiptLineItem) {
return Math.max(0, total - taxTotal)
}

return (toNumber(item.quantity) || 1) * getItemUnitPrice(item)
return unitPriceSubtotal
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

function getItemsSubtotal(items: OrderReceiptLineItem[]) {
Expand Down
43 changes: 24 additions & 19 deletions apps/medusa-be/src/modules/order-receipt/service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { orderPaymentQr } from "../../utils/order-payment-qr"
import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../payment-qr/constants"
import {
formatDate,
formatMoney,
getAddressLines,
getItemQuantity,
getItemSubtotal,
getItemTitle,
getItemUnitPrice,
Expand Down Expand Up @@ -38,7 +40,7 @@ const FONT_NORMAL = "F1" as const
const FONT_BOLD = "F2" as const
const PAYMENT_QR_MODULE_SIZE = 4
const PAYMENT_QR_X = LEFT
const PAYMENT_QR_PROVIDER_IDS = new Set(["pp_system_default"])
const PAYMENT_QR_PROVIDER_IDS = new Set([QR_PAYMENT_MEDUSA_PROVIDER_ID])
const SUPPLIER_Y = 626
const SUPPLIER_Y_WITH_PAYMENT_QR = 560

Expand Down Expand Up @@ -135,7 +137,7 @@ function buildPdf(order: OrderReceiptOrder) {

visibleItems.forEach((item, index) => {
const y = tableTop - 24 - index * 22
const quantity = toNumber(item.quantity) || 1
const quantity = getItemQuantity(item)
const lineSubtotal = getItemSubtotal(item)
const fallbackUnitPrice = getItemUnitPrice(item)
let unitPrice = fallbackUnitPrice
Expand Down Expand Up @@ -276,26 +278,29 @@ function buildPdf(order: OrderReceiptOrder) {
}

function buildPaymentQrCommands(order: OrderReceiptOrder): PdfCommand[] {
if (!isQrPaymentOrder(order)) {
return []
}
return orderPaymentQr.buildPdfCommands(getQrPaymentSpayd(order), {
moduleSize: PAYMENT_QR_MODULE_SIZE,
top: TOP,
x: PAYMENT_QR_X,
})
}

function getQrPaymentSpayd(order: OrderReceiptOrder) {
for (const collection of order.payment_collections ?? []) {
for (const payment of collection.payments ?? []) {
if (!PAYMENT_QR_PROVIDER_IDS.has(payment.provider_id ?? "")) {
continue
}

const spayd = payment.data?.payment_qr_spayd

return orderPaymentQr.buildPdfCommands(
orderPaymentQr.getSpaydFromMetadata(order.metadata),
{
moduleSize: PAYMENT_QR_MODULE_SIZE,
top: TOP,
x: PAYMENT_QR_X,
if (typeof spayd === "string" && spayd.trim()) {
return spayd
}
}
)
}
}

function isQrPaymentOrder(order: OrderReceiptOrder) {
return (order.payment_collections ?? []).some((collection) =>
(collection.payments ?? []).some((payment) =>
PAYMENT_QR_PROVIDER_IDS.has(payment.provider_id ?? "")
)
)
return null
}

class OrderReceiptModuleService {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, expect, it, vi } from "vitest"
import { QR_PAYMENT_MODULE } from "../constants"
import { QrManualPaymentProvider } from "../services/manual"

const PNG_DATA_URL_PATTERN = /^data:image\/png;base64,/

describe("QrManualPaymentProvider", () => {
it("generates SPAYD and QR image data for a payment session", async () => {
const getIban = vi.fn().mockResolvedValue("CZ65 0800 0000 1920 0014 5399")
const provider = new QrManualPaymentProvider({
[QR_PAYMENT_MODULE]: { getIban },
})

const result = await provider.initiatePayment({
amount: 1234.5,
currency_code: "czk",
context: {
idempotency_key: "1234567890",
},
})

expect(result.id).toBe("1234567890")
expect(result.status).toBe("pending")
expect(result.data?.payment_qr_spayd).toContain(
"ACC:CZ6508000000192000145399"
)
expect(result.data?.payment_qr_spayd).toContain("AM:1234.50")
expect(result.data?.payment_qr_spayd).toContain("CC:CZK")
expect(result.data?.payment_qr_spayd).toContain("X-VS:1234567890")
expect(result.data?.payment_qr_data_url).toEqual(
expect.stringMatching(PNG_DATA_URL_PATTERN)
)
expect(result.data?.qr_payment).toMatchObject({
amount: 1234.5,
currency_code: "CZK",
iban: "CZ6508000000192000145399",
reference: "1234567890",
})
})

it("loads IBAN from the qr payment module", async () => {
const getIban = vi.fn().mockResolvedValue("CZ6508000000192000145399")
const provider = new QrManualPaymentProvider({
[QR_PAYMENT_MODULE]: { getIban },
})

const result = await provider.initiatePayment({
amount: 100,
currency_code: "CZK",
data: {
reference: "987654321",
},
})

expect(getIban).toHaveBeenCalled()
expect(result.data?.payment_qr_spayd).toContain("X-VS:987654321")
})

it("preserves the existing QR payment reference when payment amount changes", async () => {
const getIban = vi.fn().mockResolvedValue("CZ6508000000192000145399")
const provider = new QrManualPaymentProvider({
[QR_PAYMENT_MODULE]: { getIban },
})

const result = await provider.updatePayment({
amount: 250,
currency_code: "CZK",
data: {
qr_payment: {
reference: "1234567890",
},
},
context: {
idempotency_key: "new-idempotency-key",
},
})

expect(result.id).toBe("1234567890")
expect(result.data?.payment_qr_spayd).toContain("AM:250.00")
expect(result.data?.payment_qr_spayd).toContain("X-VS:1234567890")
expect(result.data?.qr_payment).toMatchObject({
reference: "1234567890",
})
})

it("authorizes the manual QR payment so checkout can create an order", async () => {
const provider = new QrManualPaymentProvider({})

await expect(
provider.authorizePayment({
data: {
payment_qr_spayd: "SPD*1.0",
},
})
).resolves.toEqual({
status: "authorized",
data: {
payment_qr_spayd: "SPD*1.0",
},
})
})
})
5 changes: 5 additions & 0 deletions apps/medusa-be/src/modules/payment-qr/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const QR_PAYMENT_MODULE = "qr_payment"
export const QR_PAYMENT_CONFIG_SERVICE = "qrPaymentConfigService"
export const QR_PAYMENT_PROVIDER_IDENTIFIER = "qr_manual"
export const QR_PAYMENT_PROVIDER_ID = "default"
export const QR_PAYMENT_MEDUSA_PROVIDER_ID = `pp_${QR_PAYMENT_PROVIDER_IDENTIFIER}_${QR_PAYMENT_PROVIDER_ID}`
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Module } from "@medusajs/framework/utils"
import { QR_PAYMENT_MODULE as QR_PAYMENT_MODULE_ID } from "./constants"
import createDefaultConfigLoader from "./loaders/create-default-config"
import { QrPaymentModuleService } from "./service"

export const QR_PAYMENT_MODULE = "qr_payment"
export const QR_PAYMENT_MODULE = QR_PAYMENT_MODULE_ID

export default Module(QR_PAYMENT_MODULE, {
service: QrPaymentModuleService,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MedusaService } from "@medusajs/framework/utils"
import QrPaymentConfig from "./models/qr-payment-config"
import QrPaymentConfig from "./models/payment-qr-config"
import type { QrPaymentConfigDTO, UpdateQrPaymentConfigInput } from "./types"

export class QrPaymentModuleService extends MedusaService({
Expand Down
Loading
Loading