519 lines
16 KiB
TypeScript
519 lines
16 KiB
TypeScript
import { CreateInventoryLevelInput, ExecArgs } from "@medusajs/framework/types"
|
|
import {
|
|
ContainerRegistrationKeys,
|
|
Modules,
|
|
ProductStatus,
|
|
} from "@medusajs/framework/utils"
|
|
import {
|
|
createWorkflow,
|
|
transform,
|
|
WorkflowResponse,
|
|
} from "@medusajs/framework/workflows-sdk"
|
|
import {
|
|
createApiKeysWorkflow,
|
|
createInventoryLevelsWorkflow,
|
|
createProductsWorkflow,
|
|
createRegionsWorkflow,
|
|
createSalesChannelsWorkflow,
|
|
createShippingOptionsWorkflow,
|
|
createShippingProfilesWorkflow,
|
|
createStockLocationsWorkflow,
|
|
createTaxRegionsWorkflow,
|
|
linkSalesChannelsToApiKeyWorkflow,
|
|
linkSalesChannelsToStockLocationWorkflow,
|
|
updateStoresStep,
|
|
updateStoresWorkflow,
|
|
} from "@medusajs/medusa/core-flows"
|
|
import { ApiKey } from "../../.medusa/types/query-entry-points"
|
|
|
|
import { ALL_FORMATS, buildVariant } from "./trptk-formats"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Workflow: update store currencies
|
|
// ---------------------------------------------------------------------------
|
|
const updateStoreCurrencies = createWorkflow(
|
|
"update-store-currencies",
|
|
(input: {
|
|
supported_currencies: {
|
|
currency_code: string
|
|
is_default?: boolean
|
|
}[]
|
|
store_id: string
|
|
}) => {
|
|
const normalizedInput = transform({ input }, (data) => {
|
|
return {
|
|
selector: { id: data.input.store_id },
|
|
update: {
|
|
supported_currencies: data.input.supported_currencies.map(
|
|
(currency) => ({
|
|
currency_code: currency.currency_code,
|
|
is_default: currency.is_default ?? false,
|
|
})
|
|
),
|
|
},
|
|
}
|
|
})
|
|
|
|
const stores = updateStoresStep(normalizedInput)
|
|
return new WorkflowResponse(stores)
|
|
}
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// VAT rates for countries where we collect tax (standard rates as of 2025)
|
|
// Countries not listed here get 0% tax automatically.
|
|
// ---------------------------------------------------------------------------
|
|
const VAT_RATES: Record<string, number> = {
|
|
// EU member states
|
|
nl: 21, de: 19, fr: 20, it: 22, es: 21, be: 21, at: 20,
|
|
pl: 23, cz: 21, dk: 25, se: 25, fi: 25.5, ie: 23, pt: 23,
|
|
bg: 20, hr: 21, cy: 19, ee: 22, gr: 24, hu: 27, lv: 21,
|
|
lt: 21, lu: 17, mt: 18, ro: 19, sk: 23, si: 22,
|
|
// Non-EU European
|
|
gb: 20, ch: 8.1, no: 25,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// All supported countries (worldwide except RU and IR)
|
|
// ---------------------------------------------------------------------------
|
|
// prettier-ignore
|
|
const allCountries = [
|
|
// Europe
|
|
"al", "ad", "at", "be", "ba", "bg", "hr", "cy", "cz", "dk",
|
|
"ee", "fi", "fr", "de", "gr", "hu", "is", "ie", "it", "xk",
|
|
"lv", "li", "lt", "lu", "mk", "mt", "md", "mc", "me", "nl",
|
|
"no", "pl", "pt", "ro", "sm", "rs", "sk", "si", "es", "se",
|
|
"ch", "ua", "gb", "va",
|
|
// North America
|
|
"us", "ca", "mx",
|
|
// Central America & Caribbean
|
|
"bz", "cr", "sv", "gt", "hn", "ni", "pa",
|
|
"ag", "bs", "bb", "cu", "dm", "do", "gd", "ht", "jm", "kn",
|
|
"lc", "vc", "tt",
|
|
// South America
|
|
"ar", "bo", "br", "cl", "co", "ec", "gy", "py", "pe", "sr",
|
|
"uy", "ve",
|
|
// East Asia & Pacific
|
|
"au", "bn", "kh", "cn", "fj", "hk", "id", "jp", "kr", "la",
|
|
"mo", "my", "mn", "mm", "nz", "pg", "ph", "sg", "tw", "th",
|
|
"tl", "vn",
|
|
// South Asia
|
|
"af", "bd", "bt", "in", "mv", "np", "pk", "lk",
|
|
// Central Asia
|
|
"kz", "kg", "tj", "tm", "uz",
|
|
// Middle East (excluding IR)
|
|
"bh", "iq", "il", "jo", "kw", "lb", "om", "ps", "qa", "sa",
|
|
"ae", "ye",
|
|
// Africa
|
|
"dz", "ao", "bj", "bw", "bf", "bi", "cv", "cm", "cf", "td",
|
|
"km", "cd", "cg", "ci", "dj", "eg", "gq", "er", "sz", "et",
|
|
"ga", "gm", "gh", "gn", "gw", "ke", "ls", "lr", "ly", "mg",
|
|
"mw", "ml", "mr", "mu", "ma", "mz", "na", "ne", "ng", "rw",
|
|
"st", "sn", "sc", "sl", "so", "za", "ss", "sd", "tz", "tg",
|
|
"tn", "ug", "zm", "zw",
|
|
]
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shipping zones — country codes grouped by shipping cost tier
|
|
// ---------------------------------------------------------------------------
|
|
const ZONE_NL = ["nl"]
|
|
|
|
// prettier-ignore
|
|
const ZONE_EU1 = [
|
|
"be", "dk", "de", "fr", "it", "lu", "at", "es", "se",
|
|
]
|
|
|
|
// prettier-ignore
|
|
const ZONE_EU2 = [
|
|
"bg", "ee", "fi", "hu", "ie", "hr", "lv", "lt",
|
|
"pl", "pt", "ro", "si", "sk", "cz", "gb", "ch",
|
|
]
|
|
|
|
const ZONE_ROW = allCountries.filter(
|
|
(cc) =>
|
|
!ZONE_NL.includes(cc) &&
|
|
!ZONE_EU1.includes(cc) &&
|
|
!ZONE_EU2.includes(cc)
|
|
)
|
|
|
|
// ===========================================================================
|
|
// Main seed function
|
|
// ===========================================================================
|
|
export default async function seedTRPTKData({ container }: ExecArgs) {
|
|
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
|
const link = container.resolve(ContainerRegistrationKeys.LINK)
|
|
const query = container.resolve(ContainerRegistrationKeys.QUERY)
|
|
const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT)
|
|
const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL)
|
|
const storeModuleService = container.resolve(Modules.STORE)
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 1. Store settings
|
|
// -----------------------------------------------------------------------
|
|
logger.info("Setting up TRPTK store...")
|
|
const [store] = await storeModuleService.listStores()
|
|
|
|
let defaultSalesChannel = await salesChannelModuleService.listSalesChannels({
|
|
name: "Default Sales Channel",
|
|
})
|
|
|
|
if (!defaultSalesChannel.length) {
|
|
const { result: salesChannelResult } = await createSalesChannelsWorkflow(
|
|
container
|
|
).run({
|
|
input: {
|
|
salesChannelsData: [
|
|
{
|
|
name: "Default Sales Channel",
|
|
},
|
|
],
|
|
},
|
|
})
|
|
defaultSalesChannel = salesChannelResult
|
|
}
|
|
|
|
await updateStoreCurrencies(container).run({
|
|
input: {
|
|
store_id: store.id,
|
|
supported_currencies: [
|
|
{
|
|
currency_code: "eur",
|
|
is_default: true,
|
|
},
|
|
],
|
|
},
|
|
})
|
|
|
|
await updateStoresWorkflow(container).run({
|
|
input: {
|
|
selector: { id: store.id },
|
|
update: {
|
|
name: "TRPTK",
|
|
default_sales_channel_id: defaultSalesChannel[0].id,
|
|
},
|
|
},
|
|
})
|
|
logger.info("Store configured: TRPTK, EUR (tax-inclusive).")
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 2. Region (single Worldwide region, EUR, tax-inclusive)
|
|
// -----------------------------------------------------------------------
|
|
logger.info("Seeding region...")
|
|
const { result: regionResult } = await createRegionsWorkflow(container).run({
|
|
input: {
|
|
regions: [
|
|
{
|
|
name: "Worldwide",
|
|
currency_code: "eur",
|
|
countries: allCountries,
|
|
is_tax_inclusive: true,
|
|
payment_providers: ["pp_system_default"],
|
|
},
|
|
],
|
|
},
|
|
})
|
|
const worldwideRegion = regionResult[0]
|
|
logger.info("Region created: Worldwide.")
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 3. Tax regions with VAT rates (only for countries that collect tax)
|
|
// -----------------------------------------------------------------------
|
|
logger.info("Seeding tax regions...")
|
|
const vatCountries = Object.keys(VAT_RATES)
|
|
await createTaxRegionsWorkflow(container).run({
|
|
input: vatCountries.map((country_code) => ({
|
|
country_code,
|
|
provider_id: "tp_system",
|
|
})),
|
|
})
|
|
|
|
// Set default tax rates per country
|
|
const taxModule = container.resolve(Modules.TAX)
|
|
for (const [countryCode, rate] of Object.entries(VAT_RATES)) {
|
|
// Find the tax region for this country
|
|
const taxRegions = await taxModule.listTaxRegions({
|
|
country_code: countryCode,
|
|
})
|
|
if (taxRegions.length > 0) {
|
|
await taxModule.createTaxRates({
|
|
tax_region_id: taxRegions[0].id,
|
|
name: `VAT ${countryCode.toUpperCase()} ${rate}%`,
|
|
code: `vat-${countryCode}`,
|
|
rate,
|
|
is_default: true,
|
|
})
|
|
}
|
|
}
|
|
logger.info("Tax regions and rates created.")
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 4. Stock location
|
|
// -----------------------------------------------------------------------
|
|
logger.info("Seeding stock location...")
|
|
const { result: stockLocationResult } = await createStockLocationsWorkflow(
|
|
container
|
|
).run({
|
|
input: {
|
|
locations: [
|
|
{
|
|
name: "TRPTK Warehouse",
|
|
address: {
|
|
city: "Amsterdam",
|
|
country_code: "NL",
|
|
address_1: "",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
})
|
|
const stockLocation = stockLocationResult[0]
|
|
|
|
await updateStoresWorkflow(container).run({
|
|
input: {
|
|
selector: { id: store.id },
|
|
update: {
|
|
default_location_id: stockLocation.id,
|
|
},
|
|
},
|
|
})
|
|
|
|
await link.create({
|
|
[Modules.STOCK_LOCATION]: {
|
|
stock_location_id: stockLocation.id,
|
|
},
|
|
[Modules.FULFILLMENT]: {
|
|
fulfillment_provider_id: "manual_manual",
|
|
},
|
|
})
|
|
logger.info("Stock location created: TRPTK Warehouse.")
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 5. Fulfillment & shipping
|
|
// -----------------------------------------------------------------------
|
|
logger.info("Seeding fulfillment...")
|
|
const shippingProfiles =
|
|
await fulfillmentModuleService.listShippingProfiles({ type: "default" })
|
|
let shippingProfile = shippingProfiles.length ? shippingProfiles[0] : null
|
|
|
|
if (!shippingProfile) {
|
|
const { result: shippingProfileResult } =
|
|
await createShippingProfilesWorkflow(container).run({
|
|
input: {
|
|
data: [
|
|
{
|
|
name: "Default Shipping Profile",
|
|
type: "default",
|
|
},
|
|
],
|
|
},
|
|
})
|
|
shippingProfile = shippingProfileResult[0]
|
|
}
|
|
|
|
const toGeoZones = (countries: string[]) =>
|
|
countries.map((country_code) => ({
|
|
country_code,
|
|
type: "country" as const,
|
|
}))
|
|
|
|
const fulfillmentSet = await fulfillmentModuleService.createFulfillmentSets({
|
|
name: "TRPTK Shipping",
|
|
type: "shipping",
|
|
service_zones: [
|
|
{ name: "Netherlands", geo_zones: toGeoZones(ZONE_NL) },
|
|
{ name: "EU-1", geo_zones: toGeoZones(ZONE_EU1) },
|
|
{ name: "EU-2", geo_zones: toGeoZones(ZONE_EU2) },
|
|
{ name: "Rest of the World", geo_zones: toGeoZones(ZONE_ROW) },
|
|
],
|
|
})
|
|
|
|
await link.create({
|
|
[Modules.STOCK_LOCATION]: {
|
|
stock_location_id: stockLocation.id,
|
|
},
|
|
[Modules.FULFILLMENT]: {
|
|
fulfillment_set_id: fulfillmentSet.id,
|
|
},
|
|
})
|
|
|
|
// Shipping options: 2 per zone (Mailbox Package + Package)
|
|
const findZone = (name: string) => {
|
|
const zone = fulfillmentSet.service_zones.find((z) => z.name === name)
|
|
if (!zone) throw new Error(`Service zone "${name}" not found`)
|
|
return zone
|
|
}
|
|
|
|
const SHIPPING_ZONES = [
|
|
{ zone: findZone("Netherlands"), mailboxPrice: 0, packagePrice: 0 },
|
|
{ zone: findZone("EU-1"), mailboxPrice: 700, packagePrice: 900 },
|
|
{ zone: findZone("EU-2"), mailboxPrice: 800, packagePrice: 1200 },
|
|
{ zone: findZone("Rest of the World"), mailboxPrice: 1000, packagePrice: 2200 },
|
|
]
|
|
|
|
const shippingOptionInputs = SHIPPING_ZONES.flatMap(
|
|
({ zone, mailboxPrice, packagePrice }) => [
|
|
{
|
|
name: `Mailbox Package — ${zone.name}`,
|
|
price_type: "flat" as const,
|
|
provider_id: "manual_manual",
|
|
service_zone_id: zone.id,
|
|
shipping_profile_id: shippingProfile!.id,
|
|
type: {
|
|
label: "Mailbox Package",
|
|
description:
|
|
"For orders with CDs/SACDs only (up to 4 items). Fits through the mailbox.",
|
|
code: "mailbox-package",
|
|
},
|
|
prices: [
|
|
{ currency_code: "eur", amount: mailboxPrice },
|
|
{ region_id: worldwideRegion.id, amount: mailboxPrice },
|
|
],
|
|
rules: [
|
|
{ attribute: "enabled_in_store", value: "true", operator: "eq" as const },
|
|
{ attribute: "is_return", value: "false", operator: "eq" as const },
|
|
],
|
|
},
|
|
{
|
|
name: `Package — ${zone.name}`,
|
|
price_type: "flat" as const,
|
|
provider_id: "manual_manual",
|
|
service_zone_id: zone.id,
|
|
shipping_profile_id: shippingProfile!.id,
|
|
type: {
|
|
label: "Package",
|
|
description:
|
|
"For orders with LPs, 5+ CDs/SACDs, or mixed physical formats.",
|
|
code: "package",
|
|
},
|
|
prices: [
|
|
{ currency_code: "eur", amount: packagePrice },
|
|
{ region_id: worldwideRegion.id, amount: packagePrice },
|
|
],
|
|
rules: [
|
|
{ attribute: "enabled_in_store", value: "true", operator: "eq" as const },
|
|
{ attribute: "is_return", value: "false", operator: "eq" as const },
|
|
],
|
|
},
|
|
]
|
|
)
|
|
|
|
await createShippingOptionsWorkflow(container).run({
|
|
input: shippingOptionInputs,
|
|
})
|
|
logger.info(`Fulfillment configured: 4 zones, ${shippingOptionInputs.length} shipping options.`)
|
|
|
|
// Link sales channel to stock location
|
|
await linkSalesChannelsToStockLocationWorkflow(container).run({
|
|
input: {
|
|
id: stockLocation.id,
|
|
add: [defaultSalesChannel[0].id],
|
|
},
|
|
})
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 6. Publishable API key
|
|
// -----------------------------------------------------------------------
|
|
logger.info("Seeding API key...")
|
|
let publishableApiKey: ApiKey | null = null
|
|
const { data } = await query.graph({
|
|
entity: "api_key",
|
|
fields: ["id"],
|
|
filters: {
|
|
type: "publishable",
|
|
},
|
|
})
|
|
|
|
publishableApiKey = data?.[0]
|
|
|
|
if (!publishableApiKey) {
|
|
const {
|
|
result: [publishableApiKeyResult],
|
|
} = await createApiKeysWorkflow(container).run({
|
|
input: {
|
|
api_keys: [
|
|
{
|
|
title: "TRPTK Webshop",
|
|
type: "publishable",
|
|
created_by: "",
|
|
},
|
|
],
|
|
},
|
|
})
|
|
|
|
publishableApiKey = publishableApiKeyResult as ApiKey
|
|
}
|
|
|
|
await linkSalesChannelsToApiKeyWorkflow(container).run({
|
|
input: {
|
|
id: publishableApiKey.id,
|
|
add: [defaultSalesChannel[0].id],
|
|
},
|
|
})
|
|
logger.info("API key configured.")
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 7. Sample product: TTK0001
|
|
// -----------------------------------------------------------------------
|
|
logger.info("Seeding sample product...")
|
|
|
|
await createProductsWorkflow(container).run({
|
|
input: {
|
|
products: [
|
|
{
|
|
title: "Sample Release — TTK0001",
|
|
description:
|
|
"A sample TRPTK release to demonstrate the product structure. Replace with real catalogue data.",
|
|
handle: "ttk0001",
|
|
status: ProductStatus.PUBLISHED,
|
|
external_id: "TTK0001",
|
|
shipping_profile_id: shippingProfile!.id,
|
|
metadata: {
|
|
ean: "0608917722017",
|
|
catalogue_number: "TTK0001",
|
|
},
|
|
options: [
|
|
{
|
|
title: "Format",
|
|
values: ALL_FORMATS,
|
|
},
|
|
],
|
|
variants: ALL_FORMATS.map((format) => buildVariant("TTK0001", format)),
|
|
sales_channels: [
|
|
{
|
|
id: defaultSalesChannel[0].id,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
})
|
|
logger.info("Sample product created: TTK0001.")
|
|
|
|
// -----------------------------------------------------------------------
|
|
// 8. Inventory levels (physical variants only)
|
|
// -----------------------------------------------------------------------
|
|
logger.info("Seeding inventory levels...")
|
|
const { data: inventoryItems } = await query.graph({
|
|
entity: "inventory_item",
|
|
fields: ["id"],
|
|
})
|
|
|
|
const inventoryLevels: CreateInventoryLevelInput[] = inventoryItems.map(
|
|
(item) => ({
|
|
location_id: stockLocation.id,
|
|
stocked_quantity: 100,
|
|
inventory_item_id: item.id,
|
|
})
|
|
)
|
|
|
|
if (inventoryLevels.length) {
|
|
await createInventoryLevelsWorkflow(container).run({
|
|
input: {
|
|
inventory_levels: inventoryLevels,
|
|
},
|
|
})
|
|
}
|
|
logger.info("Inventory levels set.")
|
|
|
|
logger.info("TRPTK seed complete!")
|
|
}
|