feat(pricing, utils, types): adds money amount to pricing module (#4909)

What:

- Adds money amount service / repo / model
- Adds money amount to entry service
- Adds tests for services
- Refreshes schema
- Update joiner config to include money amounts


RESOLVES CORE-1478
RESOLVES CORE-1479

Co-authored-by: Shahed Nasser <27354907+shahednasser@users.noreply.github.com>
Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Riqwan Thamir
2023-08-31 15:03:10 +02:00
committed by GitHub
parent 0627d06f72
commit fcb6b4f510
27 changed files with 1481 additions and 112 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/pricing": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(pricing, utils, types): adds money amount to pricing module

View File

@@ -1,2 +1,3 @@
export * from "./inventory-level-stock-location"
export * from "./product-variant-inventory-item"
export * from "./product-variant-money-amount"

View File

@@ -0,0 +1,60 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { LINKS } from "../links"
export const ProductVariantMoneyAmount: ModuleJoinerConfig = {
serviceName: LINKS.ProductVariantMoneyAmount,
isLink: true,
databaseConfig: {
tableName: "product_variant_money_amount",
idPrefix: "pvma",
},
alias: [
{
name: "product_variant_money_amount",
},
{
name: "product_variant_money_amounts",
},
],
primaryKeys: ["id", "variant_id", "money_amount_id"],
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "variant_id",
alias: "variant",
args: {
methodSuffix: "Variants",
},
},
{
serviceName: Modules.PRICING,
primaryKey: "id",
foreignKey: "money_amount_id",
alias: "money_amount",
deleteCascade: true,
},
],
extends: [
{
serviceName: Modules.PRODUCT,
relationship: {
serviceName: LINKS.ProductVariantMoneyAmount,
primaryKey: "variant_id",
foreignKey: "id",
alias: "prices",
isList: true,
},
},
{
serviceName: Modules.PRICING,
relationship: {
serviceName: LINKS.ProductVariantMoneyAmount,
primaryKey: "money_amount_id",
foreignKey: "id",
alias: "variant_link",
},
},
],
}

View File

@@ -93,8 +93,7 @@ export const initialize = async (
!modulesLoadedKeys.includes(primary.serviceName) ||
!modulesLoadedKeys.includes(foreign.serviceName)
) {
// TODO: This should be uncommented when all modules are done
// continue
continue
}
const moduleDefinition = getLinkModuleDefinition(
@@ -180,8 +179,7 @@ export async function runMigrations(
!modulesLoadedKeys.includes(primary.serviceName) ||
!modulesLoadedKeys.includes(foreign.serviceName)
) {
// TODO: This should be uncommented when all modules are done
// continue
continue
}
const migrate = getMigration(definition, serviceKey, primary, foreign)

View File

@@ -8,4 +8,10 @@ export const LINKS = {
Modules.INVENTORY,
"inventory_item_id"
),
ProductVariantMoneyAmount: composeLinkName(
Modules.PRODUCT,
"variant_id",
Modules.PRICING,
"money_amount_id"
),
}

View File

@@ -0,0 +1,23 @@
export const defaultMoneyAmountsData = [
{
id: "money-amount-USD",
currency_code: "USD",
amount: 500,
min_quantity: 1,
max_quantity: 10,
},
{
id: "money-amount-EUR",
currency_code: "EUR",
amount: 400,
min_quantity: 1,
max_quantity: 5,
},
{
id: "money-amount-CAD",
currency_code: "CAD",
amount: 600,
min_quantity: 1,
max_quantity: 8,
},
]

View File

@@ -0,0 +1,20 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { MoneyAmount } from "@models"
import { defaultMoneyAmountsData } from "./data"
export async function createMoneyAmounts(
manager: SqlEntityManager,
moneyAmountsData: any[] = defaultMoneyAmountsData
): Promise<MoneyAmount[]> {
const moneyAmounts: MoneyAmount[] = []
for (let moneyAmountData of moneyAmountsData) {
const moneyAmount = manager.create(MoneyAmount, moneyAmountData)
moneyAmounts.push(moneyAmount)
}
await manager.persistAndFlush(moneyAmounts)
return moneyAmounts
}

View File

@@ -0,0 +1,335 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Currency, MoneyAmount } from "@models"
import { MoneyAmountRepository } from "@repositories"
import { MoneyAmountService } from "@services"
import { createCurrencies } from "../../../__fixtures__/currency"
import { createMoneyAmounts } from "../../../__fixtures__/money-amount"
import { MikroOrmWrapper } from "../../../utils"
jest.setTimeout(30000)
describe("MoneyAmount Service", () => {
let service: MoneyAmountService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
let data!: MoneyAmount[]
let currencyData!: Currency[]
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = await MikroOrmWrapper.forkManager()
const moneyAmountRepository = new MoneyAmountRepository({
manager: repositoryManager,
})
service = new MoneyAmountService({
moneyAmountRepository,
})
testManager = await MikroOrmWrapper.forkManager()
currencyData = await createCurrencies(testManager)
data = await createMoneyAmounts(testManager)
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("list", () => {
it("list moneyAmounts", async () => {
const moneyAmountsResult = await service.list()
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-USD",
amount: "500",
}),
expect.objectContaining({
id: "money-amount-EUR",
amount: "400",
}),
expect.objectContaining({
id: "money-amount-CAD",
amount: "600",
}),
])
})
it("list moneyAmounts by id", async () => {
const moneyAmountsResult = await service.list({
id: ["money-amount-USD"],
})
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-USD",
}),
])
})
it("list moneyAmounts with relations and selects", async () => {
const moneyAmountsResult = await service.list(
{
id: ["money-amount-USD"],
},
{
select: ["id", "min_quantity", "currency.code"],
relations: ["currency"],
}
)
const serialized = JSON.parse(JSON.stringify(moneyAmountsResult))
expect(serialized).toEqual([
{
id: "money-amount-USD",
min_quantity: "1",
currency_code: "USD",
currency: {
code: "USD",
},
},
])
})
})
describe("listAndCount", () => {
it("should return moneyAmounts and count", async () => {
const [moneyAmountsResult, count] = await service.listAndCount()
expect(count).toEqual(3)
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-USD",
}),
expect.objectContaining({
id: "money-amount-EUR",
}),
expect.objectContaining({
id: "money-amount-CAD",
}),
])
})
it("should return moneyAmounts and count when filtered", async () => {
const [moneyAmountsResult, count] = await service.listAndCount({
id: ["money-amount-USD"],
})
expect(count).toEqual(1)
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-USD",
}),
])
})
it("list moneyAmounts with relations and selects", async () => {
const [moneyAmountsResult, count] = await service.listAndCount(
{
id: ["money-amount-USD"],
},
{
select: ["id", "min_quantity", "currency.code"],
relations: ["currency"],
}
)
const serialized = JSON.parse(JSON.stringify(moneyAmountsResult))
expect(count).toEqual(1)
expect(serialized).toEqual([
{
id: "money-amount-USD",
min_quantity: "1",
currency_code: "USD",
currency: {
code: "USD",
},
},
])
})
it("should return moneyAmounts and count when using skip and take", async () => {
const [moneyAmountsResult, count] = await service.listAndCount(
{},
{ skip: 1, take: 1 }
)
expect(count).toEqual(3)
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-EUR",
}),
])
})
it("should return requested fields", async () => {
const [moneyAmountsResult, count] = await service.listAndCount(
{},
{
take: 1,
select: ["id"],
}
)
const serialized = JSON.parse(JSON.stringify(moneyAmountsResult))
expect(count).toEqual(3)
expect(serialized).toEqual([
{
id: "money-amount-USD",
},
])
})
})
describe("retrieve", () => {
const id = "money-amount-USD"
const amount = "500"
it("should return moneyAmount for the given id", async () => {
const moneyAmount = await service.retrieve(id)
expect(moneyAmount).toEqual(
expect.objectContaining({
id,
})
)
})
it("should throw an error when moneyAmount with id does not exist", async () => {
let error
try {
await service.retrieve("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(
"MoneyAmount with id: does-not-exist was not found"
)
})
it("should throw an error when a id is not provided", async () => {
let error
try {
await service.retrieve(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"moneyAmountId" must be defined')
})
it("should return moneyAmount based on config select param", async () => {
const moneyAmount = await service.retrieve(id, {
select: ["id", "amount"],
})
const serialized = JSON.parse(JSON.stringify(moneyAmount))
expect(serialized).toEqual({
id,
amount,
})
})
})
describe("delete", () => {
const id = "money-amount-USD"
it("should delete the moneyAmounts given an id successfully", async () => {
await service.delete([id])
const moneyAmounts = await service.list({
id: [id],
})
expect(moneyAmounts).toHaveLength(0)
})
})
describe("update", () => {
const id = "money-amount-USD"
it("should update the amount of the moneyAmount successfully", async () => {
await service.update([
{
id,
amount: 700,
},
])
const moneyAmount = await service.retrieve(id)
expect(moneyAmount.amount).toEqual("700")
})
it("should update the currency of the moneyAmount successfully", async () => {
await service.update([
{
id,
currency_code: "EUR",
},
])
const moneyAmount = await service.retrieve(id)
expect(moneyAmount.currency_code).toEqual("EUR")
expect(moneyAmount.currency?.code).toEqual("EUR")
})
it("should throw an error when a id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
amount: 666,
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
'MoneyAmount with id "does-not-exist" not found'
)
})
})
describe("create", () => {
it("should create a moneyAmount successfully", async () => {
await service.create([
{
id: "money-amount-TESM",
currency_code: "USD",
amount: 333,
min_quantity: 1,
max_quantity: 4,
},
])
const [moneyAmount] = await service.list({
id: ["money-amount-TESM"],
})
expect(moneyAmount).toEqual(
expect.objectContaining({
id: "money-amount-TESM",
currency_code: "USD",
amount: "333",
min_quantity: "1",
max_quantity: "4",
})
)
})
})
})

View File

@@ -1,11 +1,8 @@
import { initialize } from "../../../../src"
import { IPricingModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Currency } from "@models"
import { initialize } from "../../../../src"
import { createCurrencies } from "../../../__fixtures__/currency"
import { DB_URL, MikroOrmWrapper } from "../../../utils"

View File

@@ -0,0 +1,337 @@
import { IPricingModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Currency, MoneyAmount } from "@models"
import { initialize } from "../../../../src"
import { createCurrencies } from "../../../__fixtures__/currency"
import { createMoneyAmounts } from "../../../__fixtures__/money-amount"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
jest.setTimeout(30000)
describe("MoneyAmount Service", () => {
let service: IPricingModuleService
let testManager: SqlEntityManager
let repositoryManager: SqlEntityManager
let data!: MoneyAmount[]
let currencyData!: Currency[]
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = MikroOrmWrapper.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PRICING_DB_SCHEMA,
},
})
testManager = MikroOrmWrapper.forkManager()
testManager = await MikroOrmWrapper.forkManager()
currencyData = await createCurrencies(testManager)
data = await createMoneyAmounts(testManager)
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("list", () => {
it("list moneyAmounts", async () => {
const moneyAmountsResult = await service.list()
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-USD",
amount: "500",
}),
expect.objectContaining({
id: "money-amount-EUR",
amount: "400",
}),
expect.objectContaining({
id: "money-amount-CAD",
amount: "600",
}),
])
})
it("list moneyAmounts by id", async () => {
const moneyAmountsResult = await service.list({
id: ["money-amount-USD"],
})
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-USD",
}),
])
})
it("list moneyAmounts with relations and selects", async () => {
const moneyAmountsResult = await service.list(
{
id: ["money-amount-USD"],
},
{
select: ["id", "min_quantity", "currency.code"],
relations: ["currency"],
}
)
const serialized = JSON.parse(JSON.stringify(moneyAmountsResult))
expect(serialized).toEqual([
{
id: "money-amount-USD",
min_quantity: "1",
currency_code: "USD",
currency: {
code: "USD",
},
},
])
})
})
describe("listAndCount", () => {
it("should return moneyAmounts and count", async () => {
const [moneyAmountsResult, count] = await service.listAndCount()
expect(count).toEqual(3)
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-USD",
}),
expect.objectContaining({
id: "money-amount-EUR",
}),
expect.objectContaining({
id: "money-amount-CAD",
}),
])
})
it("should return moneyAmounts and count when filtered", async () => {
const [moneyAmountsResult, count] = await service.listAndCount({
id: ["money-amount-USD"],
})
expect(count).toEqual(1)
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-USD",
}),
])
})
it("list moneyAmounts with relations and selects", async () => {
const [moneyAmountsResult, count] = await service.listAndCount(
{
id: ["money-amount-USD"],
},
{
select: ["id", "min_quantity", "currency.code"],
relations: ["currency"],
}
)
const serialized = JSON.parse(JSON.stringify(moneyAmountsResult))
expect(count).toEqual(1)
expect(serialized).toEqual([
{
id: "money-amount-USD",
min_quantity: "1",
currency_code: "USD",
currency: {
code: "USD",
},
},
])
})
it("should return moneyAmounts and count when using skip and take", async () => {
const [moneyAmountsResult, count] = await service.listAndCount(
{},
{ skip: 1, take: 1 }
)
expect(count).toEqual(3)
expect(moneyAmountsResult).toEqual([
expect.objectContaining({
id: "money-amount-EUR",
}),
])
})
it("should return requested fields", async () => {
const [moneyAmountsResult, count] = await service.listAndCount(
{},
{
take: 1,
select: ["id"],
}
)
const serialized = JSON.parse(JSON.stringify(moneyAmountsResult))
expect(count).toEqual(3)
expect(serialized).toEqual([
{
id: "money-amount-USD",
},
])
})
})
describe("retrieve", () => {
const id = "money-amount-USD"
const amount = "500"
it("should return moneyAmount for the given id", async () => {
const moneyAmount = await service.retrieve(id)
expect(moneyAmount).toEqual(
expect.objectContaining({
id,
})
)
})
it("should throw an error when moneyAmount with id does not exist", async () => {
let error
try {
await service.retrieve("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(
"MoneyAmount with id: does-not-exist was not found"
)
})
it("should throw an error when a id is not provided", async () => {
let error
try {
await service.retrieve(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"moneyAmountId" must be defined')
})
it("should return moneyAmount based on config select param", async () => {
const moneyAmount = await service.retrieve(id, {
select: ["id", "amount"],
})
const serialized = JSON.parse(JSON.stringify(moneyAmount))
expect(serialized).toEqual({
id,
amount,
})
})
})
describe("delete", () => {
const id = "money-amount-USD"
it("should delete the moneyAmounts given an id successfully", async () => {
await service.delete([id])
const moneyAmounts = await service.list({
id: [id],
})
expect(moneyAmounts).toHaveLength(0)
})
})
describe("update", () => {
const id = "money-amount-USD"
it("should update the amount of the moneyAmount successfully", async () => {
await service.update([
{
id,
amount: 700,
},
])
const moneyAmount = await service.retrieve(id)
expect(moneyAmount.amount).toEqual("700")
})
it("should update the currency of the moneyAmount successfully", async () => {
await service.update([
{
id,
currency_code: "EUR",
},
])
const moneyAmount = await service.retrieve(id, {
relations: ["currency"],
})
expect(moneyAmount.currency_code).toEqual("EUR")
expect(moneyAmount.currency?.code).toEqual("EUR")
})
it("should throw an error when a id does not exist", async () => {
let error
try {
await service.update([
{
id: "does-not-exist",
amount: 666,
},
])
} catch (e) {
error = e
}
expect(error.message).toEqual(
'MoneyAmount with id "does-not-exist" not found'
)
})
})
describe("create", () => {
it("should create a moneyAmount successfully", async () => {
await service.create([
{
id: "money-amount-TESM",
currency_code: "USD",
amount: 333,
min_quantity: 1,
max_quantity: 4,
},
])
const [moneyAmount] = await service.list({
id: ["money-amount-TESM"],
})
expect(moneyAmount).toEqual(
expect.objectContaining({
id: "money-amount-TESM",
currency_code: "USD",
amount: "333",
min_quantity: "1",
max_quantity: "4",
})
)
})
})
})

View File

@@ -1,27 +1,31 @@
import { Modules } from "@medusajs/modules-sdk"
import { JoinerServiceConfig } from "@medusajs/types"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import { Currency } from "@models"
import { Currency, MoneyAmount } from "@models"
export enum LinkableKeys {
MONEY_AMOUNT_ID = "money_amount_id",
CURRENCY_CODE = "code",
CURRENCY_CODE = "currency_code",
}
export const entityNameToLinkableKeysMap: MapToConfig = {
[Currency.name]: [{ mapTo: LinkableKeys.CURRENCY_CODE, valueFrom: "code" }],
[MoneyAmount.name]: [
{ mapTo: LinkableKeys.MONEY_AMOUNT_ID, valueFrom: "id" },
],
}
export const joinerConfig: JoinerServiceConfig = {
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.PRICING,
primaryKeys: ["code"],
primaryKeys: ["id", "currency_code"],
linkableKeys: Object.values(LinkableKeys),
alias: [
// {
// name: "money_amount",
// },
// {
// name: "money_amounts",
// },
{
name: "money_amount",
},
{
name: "money_amounts",
},
{
name: "currency",
args: {

View File

@@ -1,7 +1,11 @@
import { ModulesSdkTypes } from "@medusajs/types"
import * as defaultRepositories from "@repositories"
import { BaseRepository, CurrencyRepository } from "@repositories"
import { CurrencyService } from "@services"
import {
BaseRepository,
CurrencyRepository,
MoneyAmountRepository,
} from "@repositories"
import { CurrencyService, MoneyAmountService } from "@services"
import { LoaderOptions } from "@medusajs/modules-sdk"
import { loadCustomRepositories } from "@medusajs/utils"
@@ -20,6 +24,7 @@ export default async ({
container.register({
currencyService: asClass(CurrencyService).singleton(),
moneyAmountService: asClass(MoneyAmountService).singleton(),
})
if (customRepositories) {
@@ -37,5 +42,6 @@ function loadDefaultRepositories({ container }) {
container.register({
baseRepository: asClass(BaseRepository).singleton(),
currencyRepository: asClass(CurrencyRepository).singleton(),
moneyAmountRepository: asClass(MoneyAmountRepository).singleton(),
})
}

View File

@@ -0,0 +1,150 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"code": {
"name": "code",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"symbol": {
"name": "symbol",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"symbol_native": {
"name": "symbol_native",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"name": {
"name": "name",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
}
},
"name": "currency",
"schema": "public",
"indexes": [
{
"keyName": "currency_pkey",
"columnNames": [
"code"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"currency_code": {
"name": "currency_code",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"amount": {
"name": "amount",
"type": "numeric",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
},
"min_quantity": {
"name": "min_quantity",
"type": "numeric",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
},
"max_quantity": {
"name": "max_quantity",
"type": "numeric",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
}
},
"name": "money_amount",
"schema": "public",
"indexes": [
{
"columnNames": [
"currency_code"
],
"composite": false,
"keyName": "IDX_money_amount_currency_code",
"primary": false,
"unique": false
},
{
"keyName": "money_amount_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"money_amount_currency_code_foreign": {
"constraintName": "money_amount_currency_code_foreign",
"columnNames": [
"currency_code"
],
"localTableName": "public.money_amount",
"referencedColumnNames": [
"code"
],
"referencedTableName": "public.currency",
"deleteRule": "set null",
"updateRule": "cascade"
}
}
}
]
}

View File

@@ -1,63 +0,0 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"code": {
"name": "code",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"symbol": {
"name": "symbol",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"symbol_native": {
"name": "symbol_native",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"name": {
"name": "name",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
}
},
"name": "currency",
"schema": "public",
"indexes": [
{
"keyName": "currency_pkey",
"columnNames": [
"code"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {}
}
]
}

View File

@@ -1,9 +0,0 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20230828182018 extends Migration {
async up(): Promise<void> {
this.addSql('create table "currency" ("code" text not null, "symbol" text not null, "symbol_native" text not null, "name" text not null, constraint "currency_pkey" primary key ("code"));');
}
}

View File

@@ -0,0 +1,14 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20230830085850 extends Migration {
async up(): Promise<void> {
this.addSql('create table "currency" ("code" text not null, "symbol" text not null, "symbol_native" text not null, "name" text not null, constraint "currency_pkey" primary key ("code"));');
this.addSql('create table "money_amount" ("id" text not null, "currency_code" text null, "amount" numeric null, "min_quantity" numeric null, "max_quantity" numeric null, constraint "money_amount_pkey" primary key ("id"));');
this.addSql('create index "IDX_money_amount_currency_code" on "money_amount" ("currency_code");');
this.addSql('alter table "money_amount" add constraint "money_amount_currency_code_foreign" foreign key ("currency_code") references "currency" ("code") on update cascade on delete set null;');
}
}

View File

@@ -1 +1,2 @@
export { default as Currency } from "./currency"
export { default as MoneyAmount } from "./money-amount"

View File

@@ -0,0 +1,42 @@
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Entity,
ManyToOne,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Currency from "./currency"
@Entity()
class MoneyAmount {
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text", nullable: true })
currency_code?: string
@ManyToOne(() => Currency, {
nullable: true,
index: "IDX_money_amount_currency_code",
fieldName: "currency_code",
})
currency?: Currency
@Property({ columnType: "numeric", nullable: true })
amount?: number
@Property({ columnType: "numeric", nullable: true })
min_quantity?: number | null
@Property({ columnType: "numeric", nullable: true })
max_quantity?: number | null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "ma")
}
}
export default MoneyAmount

View File

@@ -1,2 +1,3 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
export { CurrencyRepository } from "./currency"
export { MoneyAmountRepository } from "./money-amount"

View File

@@ -0,0 +1,127 @@
import {
Context,
CreateMoneyAmountDTO,
DAL,
UpdateMoneyAmountDTO,
} from "@medusajs/types"
import { DALUtils, MedusaError } from "@medusajs/utils"
import {
LoadStrategy,
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
} from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { MoneyAmount } from "@models"
export class MoneyAmountRepository extends DALUtils.MikroOrmBaseRepository {
protected readonly manager_: SqlEntityManager
constructor({ manager }: { manager: SqlEntityManager }) {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
super(...arguments)
this.manager_ = manager
}
async find(
findOptions: DAL.FindOptions<MoneyAmount> = { where: {} },
context: Context = {}
): Promise<MoneyAmount[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN,
})
return await manager.find(
MoneyAmount,
findOptions_.where as MikroFilterQuery<MoneyAmount>,
findOptions_.options as MikroOptions<MoneyAmount>
)
}
async findAndCount(
findOptions: DAL.FindOptions<MoneyAmount> = { where: {} },
context: Context = {}
): Promise<[MoneyAmount[], number]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const findOptions_ = { ...findOptions }
findOptions_.options ??= {}
Object.assign(findOptions_.options, {
strategy: LoadStrategy.SELECT_IN,
})
return await manager.findAndCount(
MoneyAmount,
findOptions_.where as MikroFilterQuery<MoneyAmount>,
findOptions_.options as MikroOptions<MoneyAmount>
)
}
async delete(ids: string[], context: Context = {}): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context)
await manager.nativeDelete(MoneyAmount, { id: { $in: ids } }, {})
}
async create(
data: CreateMoneyAmountDTO[],
context: Context = {}
): Promise<MoneyAmount[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const moneyAmounts = data.map((moneyAmountData) => {
return manager.create(MoneyAmount, moneyAmountData)
})
manager.persist(moneyAmounts)
return moneyAmounts
}
async update(
data: UpdateMoneyAmountDTO[],
context: Context = {}
): Promise<MoneyAmount[]> {
const manager = this.getActiveManager<SqlEntityManager>(context)
const moneyAmountIds = data.map((moneyAmountData) => moneyAmountData.id)
const existingMoneyAmounts = await this.find(
{
where: {
id: {
$in: moneyAmountIds,
},
},
},
context
)
const existingMoneyAmountMap = new Map(
existingMoneyAmounts.map<[string, MoneyAmount]>((moneyAmount) => [
moneyAmount.id,
moneyAmount,
])
)
const moneyAmounts = data.map((moneyAmountData) => {
const existingMoneyAmount = existingMoneyAmountMap.get(moneyAmountData.id)
if (!existingMoneyAmount) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`MoneyAmount with id "${moneyAmountData.id}" not found`
)
}
return manager.assign(existingMoneyAmount, moneyAmountData)
})
manager.persist(moneyAmounts)
return moneyAmounts
}
}

View File

@@ -22,14 +22,14 @@ export async function run({
logger.info(`Loading seed data from ${path}...`)
const { currenciesData } = await import(resolve(process.cwd(), path)).catch(
(e) => {
logger?.error(
`Failed to load seed data from ${path}. Please, provide a relative path and check that you export the following currenciesData.${EOL}${e}`
)
throw e
}
)
const { currenciesData, moneyAmountsData } = await import(
resolve(process.cwd(), path)
).catch((e) => {
logger?.error(
`Failed to load seed data from ${path}. Please, provide a relative path and check that you export the following: currenciesData, moneyAmountsData.${EOL}${e}`
)
throw e
})
const dbData = ModulesSdkUtils.loadDatabaseConfig("pricing", options)!
const entities = Object.values(PricingModels) as unknown as EntitySchema[]
@@ -40,12 +40,14 @@ export async function run({
entities,
pathToMigrations
)
const manager = orm.em.fork()
try {
logger.info("Inserting currencies")
logger.info("Inserting currencies & money_amounts")
await createCurrencies(manager, currenciesData)
await createMoneyAmounts(manager, moneyAmountsData)
} catch (e) {
logger.error(
`Failed to insert the seed data in the PostgreSQL database ${dbData.clientUrl}.${EOL}${e}`
@@ -59,7 +61,7 @@ async function createCurrencies(
manager: SqlEntityManager,
data: RequiredEntityData<PricingModels.Currency>[]
) {
const currencies: any[] = data.map((currencyData) => {
const currencies = data.map((currencyData) => {
return manager.create(PricingModels.Currency, currencyData)
})
@@ -67,3 +69,16 @@ async function createCurrencies(
return currencies
}
async function createMoneyAmounts(
manager: SqlEntityManager,
data: RequiredEntityData<PricingModels.MoneyAmount>[]
) {
const moneyAmounts = data.map((moneyAmountData) => {
return manager.create(PricingModels.MoneyAmount, moneyAmountData)
})
await manager.persistAndFlush(moneyAmounts)
return moneyAmounts
}

View File

@@ -1,2 +1,3 @@
export { default as CurrencyService } from "./currency"
export { default as MoneyAmountService } from "./money-amount"
export { default as PricingModuleService } from "./pricing-module"

View File

@@ -0,0 +1,111 @@
import { Context, DAL, FindConfig, PricingTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { MoneyAmount } from "@models"
import { MoneyAmountRepository } from "@repositories"
import { doNotForceTransaction, shouldForceTransaction } from "@medusajs/utils"
type InjectedDependencies = {
moneyAmountRepository: DAL.RepositoryService
}
export default class MoneyAmountService<
TEntity extends MoneyAmount = MoneyAmount
> {
protected readonly moneyAmountRepository_: DAL.RepositoryService
constructor({ moneyAmountRepository }: InjectedDependencies) {
this.moneyAmountRepository_ = moneyAmountRepository
}
@InjectManager("moneyAmountRepository_")
async retrieve(
moneyAmountId: string,
config: FindConfig<PricingTypes.MoneyAmountDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<MoneyAmount, PricingTypes.MoneyAmountDTO>({
id: moneyAmountId,
entityName: MoneyAmount.name,
repository: this.moneyAmountRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("moneyAmountRepository_")
async list(
filters: PricingTypes.FilterableMoneyAmountProps = {},
config: FindConfig<PricingTypes.MoneyAmountDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await this.moneyAmountRepository_.find(
this.buildQueryForList(filters, config),
sharedContext
)) as TEntity[]
}
@InjectManager("moneyAmountRepository_")
async listAndCount(
filters: PricingTypes.FilterableMoneyAmountProps = {},
config: FindConfig<PricingTypes.MoneyAmountDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
return (await this.moneyAmountRepository_.findAndCount(
this.buildQueryForList(filters, config),
sharedContext
)) as [TEntity[], number]
}
private buildQueryForList(
filters: PricingTypes.FilterableMoneyAmountProps = {},
config: FindConfig<PricingTypes.MoneyAmountDTO> = {}
) {
const queryOptions = ModulesSdkUtils.buildQuery<MoneyAmount>(
filters,
config
)
if (filters.id) {
queryOptions.where["id"] = { $in: filters.id }
}
return queryOptions
}
@InjectTransactionManager(shouldForceTransaction, "moneyAmountRepository_")
async create(
data: PricingTypes.CreateMoneyAmountDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.moneyAmountRepository_ as MoneyAmountRepository).create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(shouldForceTransaction, "moneyAmountRepository_")
async update(
data: PricingTypes.UpdateMoneyAmountDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.moneyAmountRepository_ as MoneyAmountRepository).update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager(doNotForceTransaction, "moneyAmountRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.moneyAmountRepository_.delete(ids, sharedContext)
}
}

View File

@@ -3,11 +3,11 @@ import {
DAL,
FindConfig,
InternalModuleDeclaration,
JoinerServiceConfig,
ModuleJoinerConfig,
PricingTypes,
} from "@medusajs/types"
import { Currency } from "@models"
import { CurrencyService } from "@services"
import { Currency, MoneyAmount } from "@models"
import { CurrencyService, MoneyAmountService } from "@services"
import {
InjectManager,
@@ -21,26 +21,142 @@ import { joinerConfig } from "../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
currencyService: CurrencyService<any>
moneyAmountService: MoneyAmountService<any>
}
export default class PricingModuleService<TCurrency extends Currency = Currency>
implements PricingTypes.IPricingModuleService
export default class PricingModuleService<
TMoneyAmount extends MoneyAmount = MoneyAmount,
TCurrency extends Currency = Currency
> implements PricingTypes.IPricingModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly currencyService_: CurrencyService<TCurrency>
protected readonly moneyAmountService_: MoneyAmountService<TMoneyAmount>
constructor(
{ baseRepository, currencyService }: InjectedDependencies,
{
baseRepository,
moneyAmountService,
currencyService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.baseRepository_ = baseRepository
this.currencyService_ = currencyService
this.moneyAmountService_ = moneyAmountService
}
__joinerConfig(): JoinerServiceConfig {
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
@InjectManager("baseRepository_")
async retrieve(
id: string,
config: FindConfig<PricingTypes.MoneyAmountDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.MoneyAmountDTO> {
const moneyAmount = await this.moneyAmountService_.retrieve(
id,
config,
sharedContext
)
return this.baseRepository_.serialize<PricingTypes.MoneyAmountDTO>(
moneyAmount,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async list(
filters: PricingTypes.FilterableMoneyAmountProps = {},
config: FindConfig<PricingTypes.MoneyAmountDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.MoneyAmountDTO[]> {
const moneyAmounts = await this.moneyAmountService_.list(
filters,
config,
sharedContext
)
return this.baseRepository_.serialize<PricingTypes.MoneyAmountDTO[]>(
moneyAmounts,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listAndCount(
filters: PricingTypes.FilterableMoneyAmountProps = {},
config: FindConfig<PricingTypes.MoneyAmountDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[PricingTypes.MoneyAmountDTO[], number]> {
const [moneyAmounts, count] = await this.moneyAmountService_.listAndCount(
filters,
config,
sharedContext
)
return [
await this.baseRepository_.serialize<PricingTypes.MoneyAmountDTO[]>(
moneyAmounts,
{
populate: true,
}
),
count,
]
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async create(
data: PricingTypes.CreateMoneyAmountDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const moneyAmounts = await this.moneyAmountService_.create(
data,
sharedContext
)
return this.baseRepository_.serialize<PricingTypes.MoneyAmountDTO[]>(
moneyAmounts,
{
populate: true,
}
)
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async update(
data: PricingTypes.UpdateMoneyAmountDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const moneyAmounts = await this.moneyAmountService_.update(
data,
sharedContext
)
return this.baseRepository_.serialize<PricingTypes.MoneyAmountDTO[]>(
moneyAmounts,
{
populate: true,
}
)
}
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.moneyAmountService_.delete(ids, sharedContext)
}
@InjectManager("baseRepository_")
async retrieveCurrency(
code: string,

View File

@@ -1 +1,2 @@
export * from "./currency"
export * from "./money-amount"

View File

@@ -0,0 +1,33 @@
import { BaseFilterable } from "../../dal"
import { CreateCurrencyDTO, CurrencyDTO } from "./currency"
export interface MoneyAmountDTO {
id: string
currency_code?: string
currency?: CurrencyDTO
amount?: number
min_quantity?: number
max_quantity?: number
}
export interface CreateMoneyAmountDTO {
id: string
currency_code: string
currency?: CreateCurrencyDTO
amount?: number
min_quantity?: number
max_quantity?: number
}
export interface UpdateMoneyAmountDTO {
id: string
currency_code?: string
amount?: number
min_quantity?: number
max_quantity?: number
}
export interface FilterableMoneyAmountProps
extends BaseFilterable<FilterableMoneyAmountProps> {
id?: string[]
}

View File

@@ -1,14 +1,49 @@
import { FindConfig } from "../common"
import { JoinerServiceConfig } from "../joiner"
import { ModuleJoinerConfig } from "../modules-sdk"
import { Context } from "../shared-context"
import {
CreateCurrencyDTO,
CreateMoneyAmountDTO,
CurrencyDTO,
FilterableCurrencyProps,
FilterableMoneyAmountProps,
MoneyAmountDTO,
UpdateCurrencyDTO,
UpdateMoneyAmountDTO,
} from "./common"
export interface IPricingModuleService {
__joinerConfig(): JoinerServiceConfig
__joinerConfig(): ModuleJoinerConfig
retrieve(
id: string,
config?: FindConfig<MoneyAmountDTO>,
sharedContext?: Context
): Promise<MoneyAmountDTO>
list(
filters?: FilterableMoneyAmountProps,
config?: FindConfig<MoneyAmountDTO>,
sharedContext?: Context
): Promise<MoneyAmountDTO[]>
listAndCount(
filters?: FilterableMoneyAmountProps,
config?: FindConfig<MoneyAmountDTO>,
sharedContext?: Context
): Promise<[MoneyAmountDTO[], number]>
create(
data: CreateMoneyAmountDTO[],
sharedContext?: Context
): Promise<MoneyAmountDTO[]>
update(
data: UpdateMoneyAmountDTO[],
sharedContext?: Context
): Promise<MoneyAmountDTO[]>
delete(ids: string[], sharedContext?: Context): Promise<void>
retrieveCurrency(
code: string,