In this new section, we’ll take a look at payments in preparation for what’s to come.
What’s the goal here?
In the same spirit as orders, we’re going to divide a single Payment into several for each store, or to be more transparent, we’re going to create dummy payments for each store.
Let me explain.
The idea behind perseides is to create a marketplace with as little friction as possible with the Meudsa core. In fact, by creating dummy payments for each child order, we’ll be able to calculate the total amount available for each seller on a specific order and we will not have to update a lot of logic.
An example is the refund of an order. With this system, the refund will recalculate the child payment (which will be shown to our seller), but also act on the parent payment, which will execute actions on the PaymentProcessor.
Extend the entity
It’s always the same recipe when it comes to extending core features: first, you extend the entity :
Create the migration
Once the entity has been extended, it’s time to migrate and apply our changes :
src/migrations/...-add-payment-children.ts
Then we build before executing the migrations :
When you refresh your database, the new column payment_parent_id
should appear:
Override the OrderService.createRefund
function
Another important aspect concerning refunds with these child payments is that when a refund is made on a child order, the payment used to refund must be the one the parent order (the real payment) :
In the code snippet above, we make sure to check whether the order is a parent order or a child order, in the case of a parent order, no worries, the original behavior remains the original, on the other hand, in the case where a vendor requests a refund from a child order, the refund must take place on the parent order
We also create “fake” refunds on that “fake” payment, allowing us for a easier computation of the refundable amount later on.
Override the OrderService.capturePayment
function
In this case, we’ll make sure that when the payment for a parent order is captured, the payments for the child orders are also captured.
When we will configure the Stripe plugin, the parent payments will be captured automatically using the medusa-payment-stripe
plugin options.
But we also needs to update child order payments :
Update the OrderPlaced subscriber
Once our Payment entity has been modified, we can update our previously created subscriber to create a Payment for each child Order created within the subscriber.
Update your code with the following :
src/subscribers/order-placed.ts
import {
LineItem,
Logger,
OrderService,
PaymentStatus,
ShippingMethod,
type SubscriberArgs,
type SubscriberConfig
} from '@medusajs/medusa'
import ShippingOptionService from 'src/services/shipping-option'
import { EntityManager } from 'typeorm'
import { Order } from '../../models/order'
import { Product } from '../../models/product'
import { Payment } from '../../models/payment'
import ProductService from '../../services/product'
export default async function handleOrderPlaced({
data,
eventName,
container,
pluginOptions,
}: SubscriberArgs<Record<string, string>>) {
const manager = container.resolve<EntityManager>('manager')
const logger = container.resolve<Logger>('logger')
await manager.transaction(async (m) => {
const orderService: OrderService = container.resolve<OrderService>('orderService')
const productService: ProductService = container.resolve<ProductService>('productService')
const shippingOptionService: ShippingOptionService =
container.resolve<ShippingOptionService>('shippingOptionService')
const orderRepo = m.getRepository(Order)
const lineItemRepo = m.getRepository(LineItem)
const shippingMethodRepo = m.getRepository(ShippingMethod)
const paymentRepo = m.getRepository(Payment)
const orderActivity = logger.activity(`Splitting order ${data.id} into child orders...`)
const order = await orderService.retrieveWithTotals(data.id, {
relations: ['items', 'items.variant', 'items.variant.prices', 'cart', 'payments', 'shipping_methods'],
})
if (!order) {
logger.failure(orderActivity, `OrderPlacedSubscriber | Order not found for order id ${data.id}.`)
return
}
const storesWithItems = new Map<string, Order['items']>()
for (const item of order.items) {
const product: Product = await productService.retrieve(item.variant.product_id)
const storeId = product.store_id
if (!storeId) {
logger.failure(orderActivity, `OrderPlacedSubscriber | product.store_id not found for product ${product.id}.`)
continue
}
if (!storesWithItems.has(storeId)) {
storesWithItems.set(storeId, [])
}
storesWithItems.get(storeId).push(item)
}
for (const [storeId, items] of storesWithItems.entries()) {
const childOrder = orderRepo.create({
...order,
order_parent_id: order.id,
store_id: storeId,
cart_id: null,
cart: null,
id: null,
shipping_methods: [],
})
const savedChildOrder = await orderRepo.save(childOrder)
let totalItemsAmount: number = 0
for (const item of items) {
const lineItem = lineItemRepo.create({
...item,
order_id: savedChildOrder.id,
cart_id: null,
id: null,
})
await lineItemRepo.save(lineItem)
totalItemsAmount += item.total
}
let totalShippingAmount: number = 0
for (const shippingMethod of order.shipping_methods) {
const shippingOption = await shippingOptionService.retrieve(shippingMethod.shipping_option_id)
if (shippingOption.store_id !== storeId) {
continue
}
const newShippingMethod = shippingMethodRepo.create({
...shippingMethod,
id: null,
cart_id: null,
cart: null,
order_id: savedChildOrder.id,
})
await shippingMethodRepo.save(newShippingMethod)
totalShippingAmount += shippingMethod.total
}
const childPayment = paymentRepo.create({
...order.payments[0],
payment_parent_id: order.payments[0].id,
order_id: savedChildOrder.id,
amount: totalItemsAmount + totalShippingAmount,
cart_id: null,
cart: null,
id: null,
})
await paymentRepo.save(childPayment)
}
await orderService.withTransaction(m).capturePayment(order.id)
logger.success(orderActivity, `Order ${data.id} has been split into ${storesWithItems.size} child orders.`)
})
}
export const config: SubscriberConfig = {
event: OrderService.Events.PLACED,
context: {
subscriberId: 'order-placed-handler',
},
}
From now on, for each order placed, a child payment will be created and linked to a child order.
In the screenshot below, here’s an example of an order that has been split into several orders.
You can see that the total amount displayed is explicitly that of the products and shipping costs linked to that specific Store only :
GitHub Branch
You can access the complete part’s code here.
Next Steps
In the next section, we’ll be integrating Stripe and Stripe Connect, so don’t forget to create your Stripe and Stripe Connect account.