From 9f725401c7de433778dee465e49c654c76a4a2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Wed, 20 May 2026 13:59:35 +0200 Subject: [PATCH 01/10] feature(payment-provider): Added payment provider for QR code. Removed QR SPAYD from product metadata --- apps/medusa-be/medusa-config.ts | 9 +- apps/medusa-be/package.json | 1 + .../src/modules/order-receipt/helpers.ts | 1 + .../src/modules/order-receipt/service.ts | 40 ++-- .../__tests__/manual-provider.unit.spec.ts | 75 ++++++ .../src/modules/qr-payment/constants.ts | 4 + .../medusa-be/src/modules/qr-payment/index.ts | 3 +- .../src/modules/qr-payment/services/manual.ts | 217 ++++++++++++++++++ apps/medusa-be/src/scripts/seed-qr-payment.ts | 134 +++++++++++ .../medusa-be/src/subscribers/order-placed.ts | 53 +---- apps/medusa-be/src/utils/order-payment-qr.ts | 59 ++--- .../workflows/send-order-payment-reminder.ts | 1 + .../src/workflows/send-order-receipt.ts | 1 + .../order-receipt/service.unit.spec.ts | 58 ++++- .../src/utils/order-payment-qr.unit.spec.ts | 41 +--- 15 files changed, 550 insertions(+), 147 deletions(-) create mode 100644 apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts create mode 100644 apps/medusa-be/src/modules/qr-payment/services/manual.ts create mode 100644 apps/medusa-be/src/scripts/seed-qr-payment.ts diff --git a/apps/medusa-be/medusa-config.ts b/apps/medusa-be/medusa-config.ts index 5665b48e3..d176ba780 100644 --- a/apps/medusa-be/medusa-config.ts +++ b/apps/medusa-be/medusa-config.ts @@ -6,6 +6,7 @@ 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_PROVIDER_ID } from "./src/modules/qr-payment/constants" loadEnv(process.env.NODE_ENV || "development", process.cwd()) @@ -29,6 +30,11 @@ const FEATURE_PACKETA_ENABLED = process.env.FEATURE_PACKETA_ENABLED === "1" const FEATURE_PAYLOAD_ENABLED = process.env.FEATURE_PAYLOAD_ENABLED === "1" const PAYKIT_PAYMENT_PROVIDERS = buildPaykitPaymentProviders() +const QR_PAYMENT_PROVIDER = { + resolve: "./src/modules/qr-payment/services/manual", + id: QR_PAYMENT_PROVIDER_ID, + options: {}, +} const NOTIFICATION_PROVIDER = process.env.NOTIFICATION_PROVIDER ?? "resend" const RESEND_API_KEY = process.env.RESEND_API_KEY @@ -377,8 +383,9 @@ module.exports = defineConfig({ }, { resolve: "@medusajs/medusa/payment", + dependencies: ["qr_payment"], options: { - providers: PAYKIT_PAYMENT_PROVIDERS, + providers: [QR_PAYMENT_PROVIDER, ...PAYKIT_PAYMENT_PROVIDERS], }, }, // PPL Client Module - config stored in DB, managed via Settings → PPL diff --git a/apps/medusa-be/package.json b/apps/medusa-be/package.json index 10907ce20..1472b0fa0 100644 --- a/apps/medusa-be/package.json +++ b/apps/medusa-be/package.json @@ -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", diff --git a/apps/medusa-be/src/modules/order-receipt/helpers.ts b/apps/medusa-be/src/modules/order-receipt/helpers.ts index f7c0c24fd..93f785958 100644 --- a/apps/medusa-be/src/modules/order-receipt/helpers.ts +++ b/apps/medusa-be/src/modules/order-receipt/helpers.ts @@ -35,6 +35,7 @@ export type OrderReceiptLineItem = { } export type OrderReceiptPayment = { + data?: Record | null provider_id?: string | null } diff --git a/apps/medusa-be/src/modules/order-receipt/service.ts b/apps/medusa-be/src/modules/order-receipt/service.ts index c9d1cd5c0..6fa2baf86 100644 --- a/apps/medusa-be/src/modules/order-receipt/service.ts +++ b/apps/medusa-be/src/modules/order-receipt/service.ts @@ -1,4 +1,5 @@ import { orderPaymentQr } from "../../utils/order-payment-qr" +import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../qr-payment/constants" import { formatDate, formatMoney, @@ -38,7 +39,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 @@ -276,26 +277,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 { diff --git a/apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts b/apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts new file mode 100644 index 000000000..6948463f9 --- /dev/null +++ b/apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts @@ -0,0 +1,75 @@ +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("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", + }, + }) + }) +}) diff --git a/apps/medusa-be/src/modules/qr-payment/constants.ts b/apps/medusa-be/src/modules/qr-payment/constants.ts index 03a4b6401..29986baa4 100644 --- a/apps/medusa-be/src/modules/qr-payment/constants.ts +++ b/apps/medusa-be/src/modules/qr-payment/constants.ts @@ -1 +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}` diff --git a/apps/medusa-be/src/modules/qr-payment/index.ts b/apps/medusa-be/src/modules/qr-payment/index.ts index 012e18d59..b2a30eb78 100644 --- a/apps/medusa-be/src/modules/qr-payment/index.ts +++ b/apps/medusa-be/src/modules/qr-payment/index.ts @@ -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, diff --git a/apps/medusa-be/src/modules/qr-payment/services/manual.ts b/apps/medusa-be/src/modules/qr-payment/services/manual.ts new file mode 100644 index 000000000..89e13f974 --- /dev/null +++ b/apps/medusa-be/src/modules/qr-payment/services/manual.ts @@ -0,0 +1,217 @@ +import type { + AuthorizePaymentInput, + AuthorizePaymentOutput, + CancelPaymentInput, + CancelPaymentOutput, + CapturePaymentInput, + CapturePaymentOutput, + DeletePaymentInput, + DeletePaymentOutput, + GetPaymentStatusInput, + GetPaymentStatusOutput, + InitiatePaymentInput, + InitiatePaymentOutput, + ProviderWebhookPayload, + RefundPaymentInput, + RefundPaymentOutput, + RetrievePaymentInput, + RetrievePaymentOutput, + UpdatePaymentInput, + UpdatePaymentOutput, + WebhookActionResult, +} from "@medusajs/framework/types" +import { + AbstractPaymentProvider, + MedusaError, + ModuleProvider, + Modules, + PaymentActions, +} from "@medusajs/framework/utils" +import QRCode from "qrcode" +import { buildPaymentQrSpayd } from "../../../utils/order-payment-qr" +import { QR_PAYMENT_MODULE, QR_PAYMENT_PROVIDER_IDENTIFIER } from "../constants" +import type { QrPaymentModuleService } from "../service" + +type QrManualPaymentProviderOptions = Record + +type QrManualPaymentProviderDependencies = { + [QR_PAYMENT_MODULE]?: QrPaymentModuleService +} + +const QR_PAYMENT_DATA_KEY = "qr_payment" +const QR_PAYMENT_SPAYD_KEY = "payment_qr_spayd" +const QR_PAYMENT_DATA_URL_KEY = "payment_qr_data_url" + +export class QrManualPaymentProvider extends AbstractPaymentProvider { + static override identifier = QR_PAYMENT_PROVIDER_IDENTIFIER + + protected readonly options_: QrManualPaymentProviderOptions + protected readonly container_: QrManualPaymentProviderDependencies + + constructor( + container: QrManualPaymentProviderDependencies, + options: QrManualPaymentProviderOptions = {} + ) { + super(container, options) + + this.container_ = container + this.options_ = options + } + + async initiatePayment( + input: InitiatePaymentInput + ): Promise { + const reference = this.getPaymentReference(input) + const iban = await this.getIban() + const amount = normalizeAmount(input.amount) + const currencyCode = input.currency_code.toUpperCase() + const message = `OBJEDNAVKA ${reference}` + const spayd = buildPaymentQrSpayd({ + amount, + currency_code: currencyCode, + iban, + message, + reference, + }) + + if (!spayd) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "QR payment requires configured IBAN and a positive payment amount" + ) + } + + const qrDataUrl = await QRCode.toDataURL(spayd, { + errorCorrectionLevel: "M", + margin: 4, + width: 320, + }) + + return { + id: reference, + status: "pending", + data: { + [QR_PAYMENT_SPAYD_KEY]: spayd, + [QR_PAYMENT_DATA_URL_KEY]: qrDataUrl, + [QR_PAYMENT_DATA_KEY]: { + amount, + currency_code: currencyCode, + iban, + message, + qr_data_url: qrDataUrl, + reference, + spayd, + }, + }, + } + } + + async authorizePayment( + input: AuthorizePaymentInput + ): Promise { + return { + status: "authorized", + data: input.data, + } + } + + async getPaymentStatus( + input: GetPaymentStatusInput + ): Promise { + return { + status: this.hasQrPaymentData(input.data) ? "authorized" : "pending", + } + } + + async retrievePayment( + input: RetrievePaymentInput + ): Promise { + return { + data: input.data, + } + } + + async updatePayment(input: UpdatePaymentInput): Promise { + return this.initiatePayment(input) + } + + async capturePayment( + input: CapturePaymentInput + ): Promise { + return { + data: input.data, + } + } + + async refundPayment(input: RefundPaymentInput): Promise { + return { + data: input.data, + } + } + + async cancelPayment(input: CancelPaymentInput): Promise { + return { + data: input.data, + } + } + + async deletePayment(input: DeletePaymentInput): Promise { + return { + data: input.data, + } + } + + async getWebhookActionAndData( + _payload: ProviderWebhookPayload["payload"] + ): Promise { + return { + action: PaymentActions.NOT_SUPPORTED, + } + } + + private async getIban() { + const qrPaymentService = this.container_[QR_PAYMENT_MODULE] + + return normalizeIban(await qrPaymentService?.getIban()) + } + + private getPaymentReference(input: InitiatePaymentInput) { + const dataReference = + getString(input.data?.reference) ?? + getString(input.data?.order_id) ?? + getString(input.context?.idempotency_key) + + return dataReference ?? `qr_${Date.now()}` + } + + private hasQrPaymentData(data: Record | undefined) { + return typeof data?.[QR_PAYMENT_SPAYD_KEY] === "string" + } +} + +export default ModuleProvider(Modules.PAYMENT, { + services: [QrManualPaymentProvider], +}) + +function normalizeAmount(amount: InitiatePaymentInput["amount"]) { + const normalized = Number(amount) + + if (!Number.isFinite(normalized) || normalized <= 0) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "QR payment amount must be positive" + ) + } + + return normalized +} + +function normalizeIban(value: string | null | undefined) { + const normalized = value?.replace(/\s+/g, "").toUpperCase() ?? "" + + return normalized || null +} + +function getString(value: unknown) { + return typeof value === "string" && value.trim() ? value.trim() : undefined +} diff --git a/apps/medusa-be/src/scripts/seed-qr-payment.ts b/apps/medusa-be/src/scripts/seed-qr-payment.ts new file mode 100644 index 000000000..507c4d7ea --- /dev/null +++ b/apps/medusa-be/src/scripts/seed-qr-payment.ts @@ -0,0 +1,134 @@ +import type { + ExecArgs, + IRegionModuleService, + Logger, + Query, + RegionDTO, +} from "@medusajs/framework/types" +import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + type SetRegionsPaymentProvidersStepInput, + setRegionsPaymentProvidersStep, +} from "@medusajs/medusa/core-flows" +import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../modules/qr-payment/constants" + +type RegionPaymentProviderLink = { + payment_provider_id: string + region_id: string +} + +const seedQrPaymentRegionsWorkflow = createWorkflow( + "seed-qr-payment-regions-workflow", + (input: SetRegionsPaymentProvidersStepInput) => { + const result = setRegionsPaymentProvidersStep(input) + + return new WorkflowResponse(result) + } +) + +export default async function seedQrPayment({ container }: ExecArgs) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + const query = container.resolve(ContainerRegistrationKeys.QUERY) + const regionService = container.resolve(Modules.REGION) + + logger.info("Starting QR payment region provider seed...") + + const regions = await listAllRegions(regionService) + if (!regions.length) { + logger.warn("No regions found. Skipping QR payment provider seed.") + return + } + + const existingLinks = await getRegionPaymentProviderLinks( + query, + regions.map((region) => region.id) + ) + const providersByRegion = toRegionPaymentProviderMap(existingLinks) + + await seedQrPaymentRegionsWorkflow(container).run({ + input: { + input: regions.map((region) => ({ + id: region.id, + payment_providers: [ + ...new Set([ + ...(providersByRegion.get(region.id) ?? ["pp_system_default"]), + QR_PAYMENT_MEDUSA_PROVIDER_ID, + ]), + ], + })), + }, + }) + + logger.info( + `QR payment region provider seed completed with provider: ${QR_PAYMENT_MEDUSA_PROVIDER_ID}` + ) +} + +async function listAllRegions(regionService: IRegionModuleService) { + const regions: RegionDTO[] = [] + const take = 100 + + for (let skip = 0; ; skip += take) { + const page = await regionService.listRegions( + {}, + { + skip, + take, + } + ) + + regions.push(...page) + + if (page.length < take) { + return regions + } + } +} + +async function getRegionPaymentProviderLinks( + query: Query, + regionIds: string[] +) { + const { data } = await query.graph({ + entity: "region_payment_provider", + fields: ["region_id", "payment_provider_id"], + filters: { + region_id: regionIds, + }, + }) + + if (!isRegionPaymentProviderLinks(data)) { + throw new Error( + "QR payment seed region provider query returned invalid row" + ) + } + + return data +} + +function toRegionPaymentProviderMap(links: RegionPaymentProviderLink[]) { + return links.reduce((map, link) => { + const providers = map.get(link.region_id) ?? [] + + providers.push(link.payment_provider_id) + map.set(link.region_id, providers) + + return map + }, new Map()) +} + +function isRegionPaymentProviderLinks( + data: unknown[] +): data is RegionPaymentProviderLink[] { + return data.every( + (link) => + typeof link === "object" && + link !== null && + typeof Reflect.get(link, "region_id") === "string" && + typeof Reflect.get(link, "payment_provider_id") === "string" + ) +} diff --git a/apps/medusa-be/src/subscribers/order-placed.ts b/apps/medusa-be/src/subscribers/order-placed.ts index c37c6780f..06cf56f77 100644 --- a/apps/medusa-be/src/subscribers/order-placed.ts +++ b/apps/medusa-be/src/subscribers/order-placed.ts @@ -1,40 +1,12 @@ -import { isDeepStrictEqual } from "node:util" import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework" -import type { - IOrderModuleService, - Logger, - Query, -} from "@medusajs/framework/types" -import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils" -import { - QR_PAYMENT_MODULE, - type QrPaymentModuleService, -} from "../modules/qr-payment" -import { - buildOrderPaymentQrMetadata, - type OrderPaymentQrOrder, -} from "../utils/order-payment-qr" +import type { Logger, Query } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" import { sendOrderReceiptWorkflow } from "../workflows/send-order-receipt" type OrderPlacedEvent = { id: string } -type OrderPlacedQueryOrder = OrderPaymentQrOrder & { - metadata?: Record | null -} - -const ORDER_PAYMENT_QR_FIELDS = [ - "id", - "currency_code", - "custom_display_id", - "display_id", - "metadata", - "summary.current_order_total", - "summary.original_order_total", - "total", -] - export default async function orderPlacedHandler({ event: { data }, container, @@ -44,26 +16,13 @@ export default async function orderPlacedHandler({ const { data: orders } = await query.graph({ entity: "order", - fields: ORDER_PAYMENT_QR_FIELDS, + fields: ["id"], filters: { id: data.id }, }) - const order = (orders as OrderPlacedQueryOrder[])[0] - - if (order) { - const qrPaymentService = - container.resolve(QR_PAYMENT_MODULE) - const metadata = buildOrderPaymentQrMetadata( - order.metadata, - order, - await qrPaymentService.getIban() - ) + const order = orders[0] - if (!isDeepStrictEqual(metadata, order.metadata)) { - const orderService = container.resolve(Modules.ORDER) - await orderService.updateOrders(order.id, { metadata }) - } - } else { - logger.warn(`Order ${data.id} was not found; payment QR metadata skipped.`) + if (!order) { + logger.warn(`Order ${data.id} was not found before receipt email.`) } await sendOrderReceiptWorkflow(container).run({ diff --git a/apps/medusa-be/src/utils/order-payment-qr.ts b/apps/medusa-be/src/utils/order-payment-qr.ts index cc5ae3f0a..ab75423ea 100644 --- a/apps/medusa-be/src/utils/order-payment-qr.ts +++ b/apps/medusa-be/src/utils/order-payment-qr.ts @@ -1,7 +1,5 @@ import QRCode from "qrcode" -export const ORDER_PAYMENT_QR_METADATA_KEY = "payment_qr_spayd" - export type OrderPaymentQrOrder = { currency_code?: string | null custom_display_id?: string | null @@ -14,6 +12,14 @@ export type OrderPaymentQrOrder = { total?: number | string | { valueOf(): unknown } | null } +export type PaymentQrPaymentData = { + amount: number | string | { valueOf(): unknown } + currency_code?: string | null + iban: string | null | undefined + message?: string | null + reference?: string | null +} + const VARIABLE_SYMBOL_REGEX = /^\d{1,10}$/ const PAYMENT_QR_QUIET_ZONE_MODULES = 4 const SPAYD_RESERVED_CHARS_REGEX = /[*:]/g @@ -56,26 +62,31 @@ export class OrderPaymentQr { return fields.join("*") } - buildMetadata( - metadata: Record | null | undefined, - order: OrderPaymentQrOrder, - iban: string | null | undefined - ) { - const spayd = this.buildSpayd(order, iban) - if (!spayd) { - return metadata + buildPaymentSpayd(payment: PaymentQrPaymentData) { + if (!payment.iban) { + return null } - return { - ...(metadata ?? {}), - [ORDER_PAYMENT_QR_METADATA_KEY]: spayd, + const amount = formatSpaydAmount(payment.amount) + if (!amount) { + return null } - } - getSpaydFromMetadata(metadata: Record | null | undefined) { - const value = metadata?.[ORDER_PAYMENT_QR_METADATA_KEY] + const fields = [ + "SPD", + "1.0", + `ACC:${payment.iban.replace(/\s+/g, "").toUpperCase()}`, + `AM:${amount}`, + `CC:${(payment.currency_code || "CZK").toUpperCase()}`, + `MSG:${escapeSpaydValue(payment.message ?? payment.reference ?? "OBJEDNAVKA")}`, + ] + + const variableSymbol = normalizeVariableSymbol(payment.reference ?? null) + if (variableSymbol) { + fields.push(`X-VS:${variableSymbol}`) + } - return typeof value === "string" && value.trim() ? value : null + return fields.join("*") } buildPdfCommands( @@ -148,18 +159,8 @@ export function buildOrderPaymentQrSpayd( return orderPaymentQr.buildSpayd(order, iban) } -export function buildOrderPaymentQrMetadata( - metadata: Record | null | undefined, - order: OrderPaymentQrOrder, - iban: string | null | undefined -) { - return orderPaymentQr.buildMetadata(metadata, order, iban) -} - -export function getOrderPaymentQrSpayd( - metadata: Record | null | undefined -) { - return orderPaymentQr.getSpaydFromMetadata(metadata) +export function buildPaymentQrSpayd(payment: PaymentQrPaymentData) { + return orderPaymentQr.buildPaymentSpayd(payment) } export function buildOrderPaymentQrPdfCommands( diff --git a/apps/medusa-be/src/workflows/send-order-payment-reminder.ts b/apps/medusa-be/src/workflows/send-order-payment-reminder.ts index cd8f25f89..b28f21f5b 100644 --- a/apps/medusa-be/src/workflows/send-order-payment-reminder.ts +++ b/apps/medusa-be/src/workflows/send-order-payment-reminder.ts @@ -58,6 +58,7 @@ const ORDER_PAYMENT_REMINDER_RECEIPT_FIELDS = [ "item_subtotal", "item_tax_total", "metadata", + "payment_collections.payments.data", "payment_collections.payments.provider_id", "shipping_total", "subtotal", diff --git a/apps/medusa-be/src/workflows/send-order-receipt.ts b/apps/medusa-be/src/workflows/send-order-receipt.ts index 58714c5ab..ece66caa7 100644 --- a/apps/medusa-be/src/workflows/send-order-receipt.ts +++ b/apps/medusa-be/src/workflows/send-order-receipt.ts @@ -52,6 +52,7 @@ const ORDER_RECEIPT_FIELDS = [ "item_subtotal", "item_tax_total", "metadata", + "payment_collections.payments.data", "payment_collections.payments.provider_id", "shipping_total", "subtotal", diff --git a/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts b/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts index 7dc8b62bf..602c8d0f3 100644 --- a/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts +++ b/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" import OrderReceiptModuleService from "../../../../../src/modules/order-receipt/service" -import { ORDER_PAYMENT_QR_METADATA_KEY } from "../../../../../src/utils/order-payment-qr" +import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../../../../../src/modules/qr-payment/constants" const baseOrder = { created_at: "2026-05-19T08:00:00.000Z", @@ -25,23 +25,25 @@ const baseOrder = { const qrPaymentCollections = [ { - payments: [{ provider_id: "pp_system_default" }], + payments: [ + { + data: { + payment_qr_spayd: + "SPD*1.0*ACC:CZ3301000000000002970297*AM:121.00*CC:CZK*MSG:OBJEDNAVKA 1234*X-VS:1234", + }, + provider_id: QR_PAYMENT_MEDUSA_PROVIDER_ID, + }, + ], }, ] -const paymentQrMetadata = { - [ORDER_PAYMENT_QR_METADATA_KEY]: - "SPD*1.0*ACC:CZ3301000000000002970297*AM:121.00*CC:CZK*MSG:OBJEDNAVKA 1234*X-VS:1234", -} - describe("order receipt service", () => { - it("renders payment QR commands when SPAYD metadata is present for a QR payment", async () => { + it("renders payment QR commands when SPAYD payment data is present for a QR payment", async () => { const service = new OrderReceiptModuleService() const withoutQr = await service.generateOrderReceiptAttachment(baseOrder) const withQr = await service.generateOrderReceiptAttachment({ ...baseOrder, - metadata: paymentQrMetadata, payment_collections: qrPaymentCollections, }) @@ -56,10 +58,17 @@ describe("order receipt service", () => { const withoutQr = await service.generateOrderReceiptAttachment(baseOrder) const withNonQrPayment = await service.generateOrderReceiptAttachment({ ...baseOrder, - metadata: paymentQrMetadata, payment_collections: [ { - payments: [{ provider_id: "pp_paykit_comgate" }], + payments: [ + { + data: { + payment_qr_spayd: + "SPD*1.0*ACC:CZ3301000000000002970297*AM:121.00*CC:CZK", + }, + provider_id: "pp_paykit_comgate", + }, + ], }, ], }) @@ -69,4 +78,31 @@ describe("order receipt service", () => { "64.00 606.00" ) }) + + it("does not render payment QR commands for the system default payment", async () => { + const service = new OrderReceiptModuleService() + + const withoutQr = await service.generateOrderReceiptAttachment(baseOrder) + const withSystemPayment = await service.generateOrderReceiptAttachment({ + ...baseOrder, + payment_collections: [ + { + payments: [ + { + data: { + payment_qr_spayd: + "SPD*1.0*ACC:CZ3301000000000002970297*AM:121.00*CC:CZK", + }, + provider_id: "pp_system_default", + }, + ], + }, + ], + }) + + expect(withSystemPayment.content.length).toBe(withoutQr.content.length) + expect(withSystemPayment.content.toString("utf8")).not.toContain( + "64.00 606.00" + ) + }) }) diff --git a/apps/medusa-be/tests/unit/src/utils/order-payment-qr.unit.spec.ts b/apps/medusa-be/tests/unit/src/utils/order-payment-qr.unit.spec.ts index ebb4646af..c1294dc89 100644 --- a/apps/medusa-be/tests/unit/src/utils/order-payment-qr.unit.spec.ts +++ b/apps/medusa-be/tests/unit/src/utils/order-payment-qr.unit.spec.ts @@ -1,9 +1,6 @@ import QRCode from "qrcode" import { describe, expect, it, vi } from "vitest" -import { - ORDER_PAYMENT_QR_METADATA_KEY, - OrderPaymentQr, -} from "../../../../src/utils/order-payment-qr" +import { OrderPaymentQr } from "../../../../src/utils/order-payment-qr" describe("order payment QR", () => { const orderPaymentQr = new OrderPaymentQr() @@ -39,25 +36,6 @@ describe("order payment QR", () => { ).toContain("*X-VS:20260001") }) - it("merges generated SPAYD string into existing metadata", () => { - expect( - orderPaymentQr.buildMetadata( - { source: "checkout" }, - { - currency_code: "CZK", - display_id: 987, - id: "order_987", - total: 42, - }, - "CZ3301000000000002970297" - ) - ).toEqual({ - source: "checkout", - [ORDER_PAYMENT_QR_METADATA_KEY]: - "SPD*1.0*ACC:CZ3301000000000002970297*AM:42.00*CC:CZK*MSG:OBJEDNAVKA 987*X-VS:987", - }) - }) - it("matches the official Czech generator shape for domestic accounts", () => { expect( orderPaymentQr.buildSpayd( @@ -104,23 +82,6 @@ describe("order payment QR", () => { ).toContain("*CC:CZK*") }) - it("leaves metadata unchanged when IBAN is not configured", () => { - const metadata = { source: "checkout" } - - expect( - orderPaymentQr.buildMetadata( - metadata, - { - currency_code: "CZK", - display_id: 987, - id: "order_987", - total: 42, - }, - undefined - ) - ).toBe(metadata) - }) - it("builds PDF drawing commands from a SPAYD string", () => { const commands = orderPaymentQr.buildPdfCommands( "SPD*1.0*ACC:CZ3301000000000002970297*AM:42.00*CC:CZK", From 55d6d9b95ca80da7a46e6fcd8c229188f0066d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Wed, 20 May 2026 14:57:56 +0200 Subject: [PATCH 02/10] feature(provider): CR fixes --- .../__tests__/manual-provider.unit.spec.ts | 27 +++++++++++++++++++ .../src/modules/qr-payment/services/manual.ts | 14 ++++++++-- apps/medusa-be/src/scripts/seed-qr-payment.ts | 15 ++++++++--- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts b/apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts index 6948463f9..132a7f645 100644 --- a/apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts +++ b/apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts @@ -56,6 +56,33 @@ describe("QrManualPaymentProvider", () => { 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({}) diff --git a/apps/medusa-be/src/modules/qr-payment/services/manual.ts b/apps/medusa-be/src/modules/qr-payment/services/manual.ts index 89e13f974..010031f84 100644 --- a/apps/medusa-be/src/modules/qr-payment/services/manual.ts +++ b/apps/medusa-be/src/modules/qr-payment/services/manual.ts @@ -175,13 +175,17 @@ export class QrManualPaymentProvider extends AbstractPaymentProvider + ) { + const existingQrPayment = getRecord(input.data?.[QR_PAYMENT_DATA_KEY]) + const existingReference = getString(existingQrPayment?.reference) const dataReference = getString(input.data?.reference) ?? getString(input.data?.order_id) ?? getString(input.context?.idempotency_key) - return dataReference ?? `qr_${Date.now()}` + return existingReference ?? dataReference ?? `qr_${Date.now()}` } private hasQrPaymentData(data: Record | undefined) { @@ -215,3 +219,9 @@ function normalizeIban(value: string | null | undefined) { function getString(value: unknown) { return typeof value === "string" && value.trim() ? value.trim() : undefined } + +function getRecord(value: unknown) { + return typeof value === "object" && value !== null + ? (value as Record) + : undefined +} diff --git a/apps/medusa-be/src/scripts/seed-qr-payment.ts b/apps/medusa-be/src/scripts/seed-qr-payment.ts index 507c4d7ea..955623c0c 100644 --- a/apps/medusa-be/src/scripts/seed-qr-payment.ts +++ b/apps/medusa-be/src/scripts/seed-qr-payment.ts @@ -5,7 +5,11 @@ import type { Query, RegionDTO, } from "@medusajs/framework/types" -import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils" +import { + ContainerRegistrationKeys, + MedusaError, + Modules, +} from "@medusajs/framework/utils" import { createWorkflow, WorkflowResponse, @@ -21,6 +25,8 @@ type RegionPaymentProviderLink = { region_id: string } +const SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID = "pp_system_default" + const seedQrPaymentRegionsWorkflow = createWorkflow( "seed-qr-payment-regions-workflow", (input: SetRegionsPaymentProvidersStepInput) => { @@ -55,7 +61,9 @@ export default async function seedQrPayment({ container }: ExecArgs) { id: region.id, payment_providers: [ ...new Set([ - ...(providersByRegion.get(region.id) ?? ["pp_system_default"]), + ...(providersByRegion.get(region.id) ?? [ + SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID, + ]), QR_PAYMENT_MEDUSA_PROVIDER_ID, ]), ], @@ -102,7 +110,8 @@ async function getRegionPaymentProviderLinks( }) if (!isRegionPaymentProviderLinks(data)) { - throw new Error( + throw new MedusaError( + MedusaError.Types.INVALID_DATA, "QR payment seed region provider query returned invalid row" ) } From d736775f6f99870fd7f57a0634f25dc8ab82876a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Thu, 21 May 2026 07:22:35 +0200 Subject: [PATCH 03/10] feature(payment-qr): gate QR payment module --- apps/medusa-be/medusa-config.ts | 24 +++++++++++++------ .../src/api/admin/qr-payment-config/route.ts | 4 ++-- .../src/modules/order-receipt/service.ts | 2 +- .../__tests__/manual-provider.unit.spec.ts | 0 .../{qr-payment => payment-qr}/constants.ts | 0 .../{qr-payment => payment-qr}/index.ts | 0 .../loaders/create-default-config.ts | 0 .../migrations/.snapshot-payment-qr.json} | 0 .../migrations/Migration20260518143000.ts | 0 .../migrations/Migration20260519093000.ts | 0 .../models/payment-qr-config.ts} | 0 .../{qr-payment => payment-qr}/service.ts | 2 +- .../services/manual.ts | 0 .../{qr-payment => payment-qr}/types.ts | 0 apps/medusa-be/src/scripts/seed-qr-payment.ts | 2 +- .../order-receipt/service.unit.spec.ts | 2 +- 16 files changed, 23 insertions(+), 13 deletions(-) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/__tests__/manual-provider.unit.spec.ts (100%) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/constants.ts (100%) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/index.ts (100%) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/loaders/create-default-config.ts (100%) rename apps/medusa-be/src/modules/{qr-payment/migrations/.snapshot-qr-payment.json => payment-qr/migrations/.snapshot-payment-qr.json} (100%) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/migrations/Migration20260518143000.ts (100%) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/migrations/Migration20260519093000.ts (100%) rename apps/medusa-be/src/modules/{qr-payment/models/qr-payment-config.ts => payment-qr/models/payment-qr-config.ts} (100%) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/service.ts (95%) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/services/manual.ts (100%) rename apps/medusa-be/src/modules/{qr-payment => payment-qr}/types.ts (100%) diff --git a/apps/medusa-be/medusa-config.ts b/apps/medusa-be/medusa-config.ts index d176ba780..82d9f1ecb 100644 --- a/apps/medusa-be/medusa-config.ts +++ b/apps/medusa-be/medusa-config.ts @@ -6,7 +6,7 @@ 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_PROVIDER_ID } from "./src/modules/qr-payment/constants" +import { QR_PAYMENT_PROVIDER_ID } from "./src/modules/payment-qr/constants" loadEnv(process.env.NODE_ENV || "development", process.cwd()) @@ -28,13 +28,19 @@ const MEILISEARCH_TYPO_TOLERANCE_SETTINGS = { 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_PAYMENT_QR_ENABLED = + (process.env.FEATURE_PAYMENT_QR_ENABLED ?? "1") === "1" const PAYKIT_PAYMENT_PROVIDERS = buildPaykitPaymentProviders() const QR_PAYMENT_PROVIDER = { - resolve: "./src/modules/qr-payment/services/manual", + 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 @@ -320,9 +326,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, @@ -383,9 +393,9 @@ module.exports = defineConfig({ }, { resolve: "@medusajs/medusa/payment", - dependencies: ["qr_payment"], + dependencies: [...(FEATURE_PAYMENT_QR_ENABLED ? ["qr_payment"] : [])], options: { - providers: [QR_PAYMENT_PROVIDER, ...PAYKIT_PAYMENT_PROVIDERS], + providers: PAYMENT_PROVIDERS, }, }, // PPL Client Module - config stored in DB, managed via Settings → PPL diff --git a/apps/medusa-be/src/api/admin/qr-payment-config/route.ts b/apps/medusa-be/src/api/admin/qr-payment-config/route.ts index e91c8c948..1d38c6377 100644 --- a/apps/medusa-be/src/api/admin/qr-payment-config/route.ts +++ b/apps/medusa-be/src/api/admin/qr-payment-config/route.ts @@ -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 = ( diff --git a/apps/medusa-be/src/modules/order-receipt/service.ts b/apps/medusa-be/src/modules/order-receipt/service.ts index 6fa2baf86..c4125ed90 100644 --- a/apps/medusa-be/src/modules/order-receipt/service.ts +++ b/apps/medusa-be/src/modules/order-receipt/service.ts @@ -1,5 +1,5 @@ import { orderPaymentQr } from "../../utils/order-payment-qr" -import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../qr-payment/constants" +import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../payment-qr/constants" import { formatDate, formatMoney, diff --git a/apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts b/apps/medusa-be/src/modules/payment-qr/__tests__/manual-provider.unit.spec.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/__tests__/manual-provider.unit.spec.ts rename to apps/medusa-be/src/modules/payment-qr/__tests__/manual-provider.unit.spec.ts diff --git a/apps/medusa-be/src/modules/qr-payment/constants.ts b/apps/medusa-be/src/modules/payment-qr/constants.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/constants.ts rename to apps/medusa-be/src/modules/payment-qr/constants.ts diff --git a/apps/medusa-be/src/modules/qr-payment/index.ts b/apps/medusa-be/src/modules/payment-qr/index.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/index.ts rename to apps/medusa-be/src/modules/payment-qr/index.ts diff --git a/apps/medusa-be/src/modules/qr-payment/loaders/create-default-config.ts b/apps/medusa-be/src/modules/payment-qr/loaders/create-default-config.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/loaders/create-default-config.ts rename to apps/medusa-be/src/modules/payment-qr/loaders/create-default-config.ts diff --git a/apps/medusa-be/src/modules/qr-payment/migrations/.snapshot-qr-payment.json b/apps/medusa-be/src/modules/payment-qr/migrations/.snapshot-payment-qr.json similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/migrations/.snapshot-qr-payment.json rename to apps/medusa-be/src/modules/payment-qr/migrations/.snapshot-payment-qr.json diff --git a/apps/medusa-be/src/modules/qr-payment/migrations/Migration20260518143000.ts b/apps/medusa-be/src/modules/payment-qr/migrations/Migration20260518143000.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/migrations/Migration20260518143000.ts rename to apps/medusa-be/src/modules/payment-qr/migrations/Migration20260518143000.ts diff --git a/apps/medusa-be/src/modules/qr-payment/migrations/Migration20260519093000.ts b/apps/medusa-be/src/modules/payment-qr/migrations/Migration20260519093000.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/migrations/Migration20260519093000.ts rename to apps/medusa-be/src/modules/payment-qr/migrations/Migration20260519093000.ts diff --git a/apps/medusa-be/src/modules/qr-payment/models/qr-payment-config.ts b/apps/medusa-be/src/modules/payment-qr/models/payment-qr-config.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/models/qr-payment-config.ts rename to apps/medusa-be/src/modules/payment-qr/models/payment-qr-config.ts diff --git a/apps/medusa-be/src/modules/qr-payment/service.ts b/apps/medusa-be/src/modules/payment-qr/service.ts similarity index 95% rename from apps/medusa-be/src/modules/qr-payment/service.ts rename to apps/medusa-be/src/modules/payment-qr/service.ts index cd16951b3..6cb37f7a9 100644 --- a/apps/medusa-be/src/modules/qr-payment/service.ts +++ b/apps/medusa-be/src/modules/payment-qr/service.ts @@ -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({ diff --git a/apps/medusa-be/src/modules/qr-payment/services/manual.ts b/apps/medusa-be/src/modules/payment-qr/services/manual.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/services/manual.ts rename to apps/medusa-be/src/modules/payment-qr/services/manual.ts diff --git a/apps/medusa-be/src/modules/qr-payment/types.ts b/apps/medusa-be/src/modules/payment-qr/types.ts similarity index 100% rename from apps/medusa-be/src/modules/qr-payment/types.ts rename to apps/medusa-be/src/modules/payment-qr/types.ts diff --git a/apps/medusa-be/src/scripts/seed-qr-payment.ts b/apps/medusa-be/src/scripts/seed-qr-payment.ts index 955623c0c..ba6b6776e 100644 --- a/apps/medusa-be/src/scripts/seed-qr-payment.ts +++ b/apps/medusa-be/src/scripts/seed-qr-payment.ts @@ -18,7 +18,7 @@ import { type SetRegionsPaymentProvidersStepInput, setRegionsPaymentProvidersStep, } from "@medusajs/medusa/core-flows" -import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../modules/qr-payment/constants" +import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../modules/payment-qr/constants" type RegionPaymentProviderLink = { payment_provider_id: string diff --git a/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts b/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts index 602c8d0f3..4c9a8307e 100644 --- a/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts +++ b/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" import OrderReceiptModuleService from "../../../../../src/modules/order-receipt/service" -import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../../../../../src/modules/qr-payment/constants" +import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../../../../../src/modules/payment-qr/constants" const baseOrder = { created_at: "2026-05-19T08:00:00.000Z", From 4defaf239cd52067380541a8cd87c2abb7284bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Thu, 21 May 2026 07:31:19 +0200 Subject: [PATCH 04/10] fix(provider): CR fixes --- apps/medusa-be/medusa-config.ts | 9 +++++++-- apps/medusa-be/src/subscribers/order-placed.ts | 16 ---------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/apps/medusa-be/medusa-config.ts b/apps/medusa-be/medusa-config.ts index 82d9f1ecb..b294f62f4 100644 --- a/apps/medusa-be/medusa-config.ts +++ b/apps/medusa-be/medusa-config.ts @@ -6,7 +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_PROVIDER_ID } from "./src/modules/payment-qr/constants" +import { + QR_PAYMENT_MODULE, + QR_PAYMENT_PROVIDER_ID, +} from "./src/modules/payment-qr/constants" loadEnv(process.env.NODE_ENV || "development", process.cwd()) @@ -393,7 +396,9 @@ module.exports = defineConfig({ }, { resolve: "@medusajs/medusa/payment", - dependencies: [...(FEATURE_PAYMENT_QR_ENABLED ? ["qr_payment"] : [])], + dependencies: [ + ...(FEATURE_PAYMENT_QR_ENABLED ? [QR_PAYMENT_MODULE] : []), + ], options: { providers: PAYMENT_PROVIDERS, }, diff --git a/apps/medusa-be/src/subscribers/order-placed.ts b/apps/medusa-be/src/subscribers/order-placed.ts index 06cf56f77..5fb96f396 100644 --- a/apps/medusa-be/src/subscribers/order-placed.ts +++ b/apps/medusa-be/src/subscribers/order-placed.ts @@ -1,6 +1,4 @@ import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework" -import type { Logger, Query } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" import { sendOrderReceiptWorkflow } from "../workflows/send-order-receipt" type OrderPlacedEvent = { @@ -11,20 +9,6 @@ export default async function orderPlacedHandler({ event: { data }, container, }: SubscriberArgs) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) - - const { data: orders } = await query.graph({ - entity: "order", - fields: ["id"], - filters: { id: data.id }, - }) - const order = orders[0] - - if (!order) { - logger.warn(`Order ${data.id} was not found before receipt email.`) - } - await sendOrderReceiptWorkflow(container).run({ input: { order_id: data.id, From 072f0553ff9764852038b95179fa74a4d7fe518d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Thu, 21 May 2026 07:35:21 +0200 Subject: [PATCH 05/10] fix(provider): CR fixes --- apps/medusa-be/src/modules/payment-qr/constants.ts | 1 + apps/medusa-be/src/scripts/seed-qr-payment.ts | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/medusa-be/src/modules/payment-qr/constants.ts b/apps/medusa-be/src/modules/payment-qr/constants.ts index 29986baa4..921cdb5a6 100644 --- a/apps/medusa-be/src/modules/payment-qr/constants.ts +++ b/apps/medusa-be/src/modules/payment-qr/constants.ts @@ -3,3 +3,4 @@ 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}` +export const SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID = "pp_system_default" diff --git a/apps/medusa-be/src/scripts/seed-qr-payment.ts b/apps/medusa-be/src/scripts/seed-qr-payment.ts index ba6b6776e..4e3c0793d 100644 --- a/apps/medusa-be/src/scripts/seed-qr-payment.ts +++ b/apps/medusa-be/src/scripts/seed-qr-payment.ts @@ -18,15 +18,16 @@ import { type SetRegionsPaymentProvidersStepInput, setRegionsPaymentProvidersStep, } from "@medusajs/medusa/core-flows" -import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../modules/payment-qr/constants" +import { + QR_PAYMENT_MEDUSA_PROVIDER_ID, + SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID, +} from "../modules/payment-qr/constants" type RegionPaymentProviderLink = { payment_provider_id: string region_id: string } -const SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID = "pp_system_default" - const seedQrPaymentRegionsWorkflow = createWorkflow( "seed-qr-payment-regions-workflow", (input: SetRegionsPaymentProvidersStepInput) => { From 089d4df64c69e96ae760f43314e361046bfdd3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Thu, 21 May 2026 07:40:24 +0200 Subject: [PATCH 06/10] fix(provider): CR fixes --- apps/medusa-be/medusa-config.ts | 15 ++++++--- .../src/modules/payment-qr/constants.ts | 1 - apps/medusa-be/src/scripts/seed-qr-payment.ts | 31 +++++++++++++------ .../medusa-be/src/workflows/seed/constants.ts | 1 + .../seed/paykit-payment-providers.ts | 5 ++- 5 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 apps/medusa-be/src/workflows/seed/constants.ts diff --git a/apps/medusa-be/medusa-config.ts b/apps/medusa-be/medusa-config.ts index b294f62f4..e02c8b67d 100644 --- a/apps/medusa-be/medusa-config.ts +++ b/apps/medusa-be/medusa-config.ts @@ -28,11 +28,18 @@ 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_PAYMENT_QR_ENABLED_DEFAULT = FEATURE_FLAG_ENABLED_VALUE + +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 ?? "1") === "1" + (process.env.FEATURE_PAYMENT_QR_ENABLED ?? + FEATURE_PAYMENT_QR_ENABLED_DEFAULT) === FEATURE_FLAG_ENABLED_VALUE const PAYKIT_PAYMENT_PROVIDERS = buildPaykitPaymentProviders() const QR_PAYMENT_PROVIDER = { diff --git a/apps/medusa-be/src/modules/payment-qr/constants.ts b/apps/medusa-be/src/modules/payment-qr/constants.ts index 921cdb5a6..29986baa4 100644 --- a/apps/medusa-be/src/modules/payment-qr/constants.ts +++ b/apps/medusa-be/src/modules/payment-qr/constants.ts @@ -3,4 +3,3 @@ 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}` -export const SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID = "pp_system_default" diff --git a/apps/medusa-be/src/scripts/seed-qr-payment.ts b/apps/medusa-be/src/scripts/seed-qr-payment.ts index 4e3c0793d..01bf1c79a 100644 --- a/apps/medusa-be/src/scripts/seed-qr-payment.ts +++ b/apps/medusa-be/src/scripts/seed-qr-payment.ts @@ -18,16 +18,18 @@ import { type SetRegionsPaymentProvidersStepInput, setRegionsPaymentProvidersStep, } from "@medusajs/medusa/core-flows" -import { - QR_PAYMENT_MEDUSA_PROVIDER_ID, - SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID, -} from "../modules/payment-qr/constants" +import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../modules/payment-qr/constants" +import { SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID } from "../workflows/seed/constants" type RegionPaymentProviderLink = { payment_provider_id: string region_id: string } +type RegionPaymentProviderLinkRecord = Partial< + Record +> + const seedQrPaymentRegionsWorkflow = createWorkflow( "seed-qr-payment-regions-workflow", (input: SetRegionsPaymentProvidersStepInput) => { @@ -134,11 +136,20 @@ function toRegionPaymentProviderMap(links: RegionPaymentProviderLink[]) { function isRegionPaymentProviderLinks( data: unknown[] ): data is RegionPaymentProviderLink[] { - return data.every( - (link) => - typeof link === "object" && - link !== null && - typeof Reflect.get(link, "region_id") === "string" && - typeof Reflect.get(link, "payment_provider_id") === "string" + return data.every(isRegionPaymentProviderLink) +} + +function isRegionPaymentProviderLink( + link: unknown +): link is RegionPaymentProviderLink { + if (typeof link !== "object" || link === null) { + return false + } + + const row = link as RegionPaymentProviderLinkRecord + + return ( + typeof row.region_id === "string" && + typeof row.payment_provider_id === "string" ) } diff --git a/apps/medusa-be/src/workflows/seed/constants.ts b/apps/medusa-be/src/workflows/seed/constants.ts new file mode 100644 index 000000000..360a1721d --- /dev/null +++ b/apps/medusa-be/src/workflows/seed/constants.ts @@ -0,0 +1 @@ +export const SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID = "pp_system_default" diff --git a/apps/medusa-be/src/workflows/seed/paykit-payment-providers.ts b/apps/medusa-be/src/workflows/seed/paykit-payment-providers.ts index 277a0dc14..c5cb674a4 100644 --- a/apps/medusa-be/src/workflows/seed/paykit-payment-providers.ts +++ b/apps/medusa-be/src/workflows/seed/paykit-payment-providers.ts @@ -4,8 +4,7 @@ import { PAYKIT_PAYMENT_PROVIDER_IDENTIFIER, PAYKIT_STRIPE_PROVIDER_ID, } from "../../modules/payment-paykit/config" - -const SYSTEM_PAYMENT_PROVIDER_ID = "pp_system_default" +import { SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID } from "./constants" const toMedusaPaymentProviderId = (providerId: string) => `pp_${PAYKIT_PAYMENT_PROVIDER_IDENTIFIER}_${providerId}` @@ -22,7 +21,7 @@ const mergePaymentProviders = ( ) => { const baseProviderIds = currentProviderIds?.length ? currentProviderIds - : [SYSTEM_PAYMENT_PROVIDER_ID] + : [SYSTEM_DEFAULT_PAYMENT_PROVIDER_ID] return [...new Set([...baseProviderIds, ...paykitProviderIds])] } From 24457a0a51ea5c508d9c3dec0c0ce76ddbb5cc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Thu, 21 May 2026 07:48:11 +0200 Subject: [PATCH 07/10] fix(provider): CR fixes --- apps/medusa-be/medusa-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/medusa-be/medusa-config.ts b/apps/medusa-be/medusa-config.ts index e02c8b67d..06fcfe6d5 100644 --- a/apps/medusa-be/medusa-config.ts +++ b/apps/medusa-be/medusa-config.ts @@ -29,7 +29,7 @@ const MEILISEARCH_TYPO_TOLERANCE_SETTINGS = { } const FEATURE_FLAG_ENABLED_VALUE = "1" -const FEATURE_PAYMENT_QR_ENABLED_DEFAULT = FEATURE_FLAG_ENABLED_VALUE +const FEATURE_PAYMENT_QR_ENABLED_DEFAULT = "0" const FEATURE_PPL_ENABLED = process.env.FEATURE_PPL_ENABLED === FEATURE_FLAG_ENABLED_VALUE From bfb4a3d49099375821f1c25e4b63517658dcec6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Thu, 21 May 2026 10:18:09 +0200 Subject: [PATCH 08/10] fix(receipt): correct invoice item quantities --- .../src/modules/order-receipt/helpers.ts | 20 +++++++++++++++++-- .../src/modules/order-receipt/service.ts | 3 ++- .../workflows/send-order-payment-reminder.ts | 3 +++ .../src/workflows/send-order-receipt.ts | 3 +++ .../order-receipt/service.unit.spec.ts | 18 +++++++++++++++++ docker-compose.yaml | 1 + 6 files changed, 45 insertions(+), 3 deletions(-) diff --git a/apps/medusa-be/src/modules/order-receipt/helpers.ts b/apps/medusa-be/src/modules/order-receipt/helpers.ts index 93f785958..3e5f8123a 100644 --- a/apps/medusa-be/src/modules/order-receipt/helpers.ts +++ b/apps/medusa-be/src/modules/order-receipt/helpers.ts @@ -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 @@ -263,10 +266,23 @@ 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 +} + export function getItemSubtotal(item: OrderReceiptLineItem) { + const quantity = getItemQuantity(item) + const unitPriceSubtotal = quantity * getItemUnitPrice(item) const subtotal = toNumber(item.subtotal) if (subtotal > 0) { - return subtotal + return Math.max(subtotal, unitPriceSubtotal) } const total = toNumber(item.total) @@ -275,7 +291,7 @@ export function getItemSubtotal(item: OrderReceiptLineItem) { return Math.max(0, total - taxTotal) } - return (toNumber(item.quantity) || 1) * getItemUnitPrice(item) + return unitPriceSubtotal } function getItemsSubtotal(items: OrderReceiptLineItem[]) { diff --git a/apps/medusa-be/src/modules/order-receipt/service.ts b/apps/medusa-be/src/modules/order-receipt/service.ts index c4125ed90..a6194e148 100644 --- a/apps/medusa-be/src/modules/order-receipt/service.ts +++ b/apps/medusa-be/src/modules/order-receipt/service.ts @@ -4,6 +4,7 @@ import { formatDate, formatMoney, getAddressLines, + getItemQuantity, getItemSubtotal, getItemTitle, getItemUnitPrice, @@ -136,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 diff --git a/apps/medusa-be/src/workflows/send-order-payment-reminder.ts b/apps/medusa-be/src/workflows/send-order-payment-reminder.ts index b28f21f5b..e62aca882 100644 --- a/apps/medusa-be/src/workflows/send-order-payment-reminder.ts +++ b/apps/medusa-be/src/workflows/send-order-payment-reminder.ts @@ -65,8 +65,11 @@ const ORDER_PAYMENT_REMINDER_RECEIPT_FIELDS = [ "tax_total", "total", "items.detail.raw_unit_price", + "items.detail.quantity", + "items.detail.raw_quantity", "items.detail.title", "items.detail.unit_price", + "items.raw_quantity", "items.raw_unit_price", "items.subtotal", "items.tax_total", diff --git a/apps/medusa-be/src/workflows/send-order-receipt.ts b/apps/medusa-be/src/workflows/send-order-receipt.ts index ece66caa7..68249df99 100644 --- a/apps/medusa-be/src/workflows/send-order-receipt.ts +++ b/apps/medusa-be/src/workflows/send-order-receipt.ts @@ -59,8 +59,11 @@ const ORDER_RECEIPT_FIELDS = [ "tax_total", "total", "items.detail.raw_unit_price", + "items.detail.quantity", + "items.detail.raw_quantity", "items.detail.title", "items.detail.unit_price", + "items.raw_quantity", "items.raw_unit_price", "items.subtotal", "items.tax_total", diff --git a/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts b/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts index 4c9a8307e..1d3345c81 100644 --- a/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts +++ b/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts @@ -1,4 +1,8 @@ import { describe, expect, it } from "vitest" +import { + getItemQuantity, + getItemSubtotal, +} from "../../../../../src/modules/order-receipt/helpers" import OrderReceiptModuleService from "../../../../../src/modules/order-receipt/service" import { QR_PAYMENT_MEDUSA_PROVIDER_ID } from "../../../../../src/modules/payment-qr/constants" @@ -38,6 +42,20 @@ const qrPaymentCollections = [ ] describe("order receipt service", () => { + it("uses detail quantity when calculating line subtotal", () => { + const item = { + detail: { + raw_quantity: { value: "2", precision: 20 }, + raw_unit_price: { value: "1652.06612", precision: 20 }, + }, + subtotal: 1652.066_12, + title: "Pánská mikina Capita SKULL HOODIE", + } + + expect(getItemQuantity(item)).toBe(2) + expect(getItemSubtotal(item)).toBeCloseTo(3304.132_24) + }) + it("renders payment QR commands when SPAYD payment data is present for a QR payment", async () => { const service = new OrderReceiptModuleService() diff --git a/docker-compose.yaml b/docker-compose.yaml index 9b73c1488..6d05d6a62 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -63,6 +63,7 @@ x-medusa-env: &medusa-env PACKETA_ENVIRONMENT: ${DC_PACKETA_ENVIRONMENT:-testing} PACKETA_PICKUP_POINTS_API_KEY: ${DC_NEXT_PUBLIC_PACKETA_WIDGET_API_KEY:-} FEATURE_PAYLOAD_ENABLED: ${DC_FEATURE_PAYLOAD_ENABLED:-0} + FEATURE_PAYMENT_QR_ENABLED: ${DC_FEATURE_PAYMENT_QR_ENABLED:-0} # Medusa BE: PayKit payment providers FEATURE_PAYKIT_ENABLED: ${DC_FEATURE_PAYKIT_ENABLED:-0} From 57f138e8b7957f386b6575b6b98d3bb9610154b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Thu, 21 May 2026 10:28:59 +0200 Subject: [PATCH 09/10] fix(receipt): preserve discounted line totals --- apps/medusa-be/src/modules/order-receipt/helpers.ts | 9 +++++++-- .../src/modules/order-receipt/service.unit.spec.ts | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/medusa-be/src/modules/order-receipt/helpers.ts b/apps/medusa-be/src/modules/order-receipt/helpers.ts index 3e5f8123a..abe8c1f7b 100644 --- a/apps/medusa-be/src/modules/order-receipt/helpers.ts +++ b/apps/medusa-be/src/modules/order-receipt/helpers.ts @@ -279,10 +279,15 @@ export function getItemQuantity(item: OrderReceiptLineItem) { export function getItemSubtotal(item: OrderReceiptLineItem) { const quantity = getItemQuantity(item) - const unitPriceSubtotal = quantity * getItemUnitPrice(item) + const unitPrice = getItemUnitPrice(item) + const unitPriceSubtotal = quantity * unitPrice const subtotal = toNumber(item.subtotal) if (subtotal > 0) { - return Math.max(subtotal, unitPriceSubtotal) + if (quantity > 1 && subtotal === unitPrice) { + return unitPriceSubtotal + } + + return subtotal } const total = toNumber(item.total) diff --git a/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts b/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts index 1d3345c81..5518b9d2f 100644 --- a/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts +++ b/apps/medusa-be/tests/unit/src/modules/order-receipt/service.unit.spec.ts @@ -56,6 +56,16 @@ describe("order receipt service", () => { expect(getItemSubtotal(item)).toBeCloseTo(3304.132_24) }) + it("keeps discounted line subtotal when it differs from unit price", () => { + expect( + getItemSubtotal({ + quantity: 2, + subtotal: 160, + unit_price: 100, + }) + ).toBe(160) + }) + it("renders payment QR commands when SPAYD payment data is present for a QR payment", async () => { const service = new OrderReceiptModuleService() From d6660e16359a7756965a3ba68caa662875e29e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Zbedina?= Date: Thu, 21 May 2026 11:40:51 +0200 Subject: [PATCH 10/10] fix(provider): simplify QR feature flag --- apps/medusa-be/medusa-config.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/medusa-be/medusa-config.ts b/apps/medusa-be/medusa-config.ts index 06fcfe6d5..ea3d63918 100644 --- a/apps/medusa-be/medusa-config.ts +++ b/apps/medusa-be/medusa-config.ts @@ -29,7 +29,6 @@ const MEILISEARCH_TYPO_TOLERANCE_SETTINGS = { } const FEATURE_FLAG_ENABLED_VALUE = "1" -const FEATURE_PAYMENT_QR_ENABLED_DEFAULT = "0" const FEATURE_PPL_ENABLED = process.env.FEATURE_PPL_ENABLED === FEATURE_FLAG_ENABLED_VALUE @@ -38,8 +37,7 @@ const FEATURE_PACKETA_ENABLED = 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_PAYMENT_QR_ENABLED_DEFAULT) === FEATURE_FLAG_ENABLED_VALUE + process.env.FEATURE_PAYMENT_QR_ENABLED === FEATURE_FLAG_ENABLED_VALUE const PAYKIT_PAYMENT_PROVIDERS = buildPaykitPaymentProviders() const QR_PAYMENT_PROVIDER = {