diff --git a/www/apps/book/app/advanced-development/admin/widgets/page.mdx b/www/apps/book/app/advanced-development/admin/widgets/page.mdx index 784393be01..b43e76a8a2 100644 --- a/www/apps/book/app/advanced-development/admin/widgets/page.mdx +++ b/www/apps/book/app/advanced-development/admin/widgets/page.mdx @@ -75,9 +75,12 @@ Widgets that are injected into a details page (for example, `product.details.aft For example: -```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["5"]]} +```tsx title="src/admin/widgets/product-widget.tsx" highlights={[["8"]]} import { defineWidgetConfig } from "@medusajs/admin-shared" -import { DetailWidgetProps, AdminProduct } from "@medusajs/types" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/types" const ProductWidget = ({ data, diff --git a/www/apps/book/app/advanced-development/api-routes/parameters/page.mdx b/www/apps/book/app/advanced-development/api-routes/parameters/page.mdx index 90a90e486f..38f5fa19ee 100644 --- a/www/apps/book/app/advanced-development/api-routes/parameters/page.mdx +++ b/www/apps/book/app/advanced-development/api-routes/parameters/page.mdx @@ -41,8 +41,8 @@ To create an API route that accepts multiple path parameters, create within the For example, create an API route at `src/api/store/hello-world/[id]/name/[name]/route.ts`: export const multiplePathHighlights = [ - ["11", "req.params.id", "Access the path parameter `id`"], - ["11", "req.params.name", "Access the path parameter `name`"] + ["12", "req.params.id", "Access the path parameter `id`"], + ["13", "req.params.name", "Access the path parameter `name`"] ] ```ts title="src/api/store/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} @@ -56,7 +56,9 @@ export const GET = ( res: MedusaResponse ) => { res.json({ - message: `[GET] Hello ${req.params.id} - ${req.params.name}!`, + message: `[GET] Hello ${ + req.params.id + } - ${req.params.name}!`, }) } ``` diff --git a/www/apps/book/app/advanced-development/api-routes/protected-routes/page.mdx b/www/apps/book/app/advanced-development/api-routes/protected-routes/page.mdx index ad1077fd4c..8e962b7d60 100644 --- a/www/apps/book/app/advanced-development/api-routes/protected-routes/page.mdx +++ b/www/apps/book/app/advanced-development/api-routes/protected-routes/page.mdx @@ -125,12 +125,15 @@ To protect custom API Routes that don’t start with `/store/customers/me` or `/ For example: export const highlights = [ - ["8", "authenticate", "Only authenticated admin users can access routes starting with `/custom/admin`"], - ["14", "authenticate", "Only authenticated customers can access routes starting with `/custom/customers`"] + ["11", "authenticate", "Only authenticated admin users can access routes starting with `/custom/admin`"], + ["17", "authenticate", "Only authenticated customers can access routes starting with `/custom/customers`"] ] ```ts title="src/api/middlewares.ts" highlights={highlights} -import { MiddlewaresConfig, authenticate } from "@medusajs/medusa" +import { + MiddlewaresConfig, + authenticate, +} from "@medusajs/medusa" export const config: MiddlewaresConfig = { routes: [ diff --git a/www/apps/book/app/advanced-development/data-models/common-definitions/page.mdx b/www/apps/book/app/advanced-development/data-models/common-definitions/page.mdx deleted file mode 100644 index 10be71e140..0000000000 --- a/www/apps/book/app/advanced-development/data-models/common-definitions/page.mdx +++ /dev/null @@ -1,89 +0,0 @@ -export const metadata = { - title: `${pageNumber} Common Data Model Definitions`, -} - -# {metadata.title} - -In this chapter, you'll learn where to find resources on common data model definitions related to field's column types, relations, and indices. - -## MikroORM Learning Resources - -Refer to the following resources to learn about common definitions: - -1. [Field column types](https://mikro-orm.io/docs/defining-entities). -2. [Creating indices](https://mikro-orm.io/docs/defining-entities#indexes). - -Refer to MikroORM's documentation for more details related to your use case. - ---- - -## Relations Between Data Models - -You can build relations between data models in the same module using foreign keys. - -Refer to [MikroORM's relations definitions](https://mikro-orm.io/docs/relationships) for more details. - - - -For building relationships between data models in different modules, refer to the [Module Relationships chapter](../../modules/module-relationships/page.mdx) instead. - - - ---- - -## Example Data Model - -The following example showcase a data model with common definitions: - -```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" -import { - Entity, - Enum, - OneToOne, - PrimaryKey, - Property, -} from "@mikro-orm/core" -import { BaseEntity } from "@medusajs/utils" -// assuming this is another implemented data model -import ProductVariant from "./product-variant" - -export enum MediaType { - MAIN = "main", - PREVIEW = "preview" -} - -@Entity() -class ProductMedia extends BaseEntity { - @PrimaryKey({ columnType: "text" }) - id: string - - @Property({ columnType: "text" }) - name: string - - @Enum({ items: ["main", "preview"] }) - type: MediaType - - @Property({ columnType: "text" }) - file_key: string - - @Property({ columnType: "text" }) - mime_type: string - - @Property({ columnType: "text" }) - variant_id: string - - @OneToOne({ - entity: ProductVariant, - onDelete: "cascade", - }) - variant: ProductVariant -} - -export default ProductMedia -``` - -In the example above: - -- The `ProductMedia` data model has the columns `id`, `name`, `type`, `file_key`, `mime_type`, and `variant_id`. -- The data model has an index on the `variant_id` column. -- The data model has a one-to-one relation to a `ProductVariant` data model. diff --git a/www/apps/book/app/advanced-development/data-models/configure-properties/page.mdx b/www/apps/book/app/advanced-development/data-models/configure-properties/page.mdx new file mode 100644 index 0000000000..f2f2b083c9 --- /dev/null +++ b/www/apps/book/app/advanced-development/data-models/configure-properties/page.mdx @@ -0,0 +1,84 @@ +export const metadata = { + title: `${pageNumber} Configure Data Model Properties`, +} + +# {metadata.title} + +In this chapter, you’ll learn how to configure data model properties, such as setting their default value. + +## Property’s Default Value + +Use the `default` method of the `model` utility to specify the default value of a property. + +For example: + +export const defaultHighlights = [ + ["6", "default", "Set the default value to `black`."], + ["9", "default", "Set the default value to `0`."] +] + +```ts highlights={defaultHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + color: model + .enum(["black", "white"]) + .default("black"), + age: model + .number() + .default(0), + // ... +}) + +export default MyCustom +``` + +In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. + +--- + +## Nullable Property + +Use the `nullable` method to indicate that a property’s value can be `null`. + +For example: + +export const nullableHighlights = [ + ["4", "nullable", "Configure the `price` property to allow `null` values."] +] + +```ts highlights={nullableHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + price: model.bigNumber().nullable(), + // ... +}) + +export default MyCustom +``` + +--- + +## Unique Property + +The `unique` method indicates that a property’s value must be unique in the database. + +For example: + +export const uniqueHighlights = [ + ["5", "unique", "Configure the `email` property to allow unique values only."] +] + +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/utils" + +const User = model.define("user", { + email: model.text().unique(), + // ... +}) + +export default User +``` + +In this example, multiple users can’t have the same email. diff --git a/www/apps/book/app/advanced-development/data-models/indexes/page.mdx b/www/apps/book/app/advanced-development/data-models/indexes/page.mdx new file mode 100644 index 0000000000..f4b6b3e2b5 --- /dev/null +++ b/www/apps/book/app/advanced-development/data-models/indexes/page.mdx @@ -0,0 +1,32 @@ +export const metadata = { + title: `${pageNumber} Data Model Indexes`, +} + +# {metadata.title} + +In this chapter, you’ll learn how to define indexes on a data model. + +## Define Index + +The `index` method defines a custom index on a property. + +For example: + +export const highlights = [ + ["5", "index", "Define an index on the `name` property."] +] + +```ts highlights={highlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + id: model.id(), + name: model.text().index( + "IDX_MY_CUSTOM_NAME" + ), +}) + +export default MyCustom +``` + +In this example, you define an index on the `name` property. The `index` method optionally accepts the name of the index as a parameter. diff --git a/www/apps/book/app/advanced-development/data-models/primary-key/page.mdx b/www/apps/book/app/advanced-development/data-models/primary-key/page.mdx new file mode 100644 index 0000000000..0803069dc3 --- /dev/null +++ b/www/apps/book/app/advanced-development/data-models/primary-key/page.mdx @@ -0,0 +1,40 @@ +export const metadata = { + title: `${pageNumber} Data Model’s Primary Key`, +} + +# {metadata.title} + +In this chapter, you’ll learn how to configure the primary key of a data model. + +## id Property + +A property defined with the `id` method is, by default, considered the data model’s primary key. + +--- + +## primaryKey Method + +To set any `text` or `number` property as a primary key, use the `primaryKey` method. + +For example: + +export const highlights = [ + ["4", "primaryKey", "Define the `code` property to be the data model's primary key."] +] + +```ts highlights={highlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + code: model.text().primaryKey(), + // ... +}) + +export default MyCustom +``` + + + +A property that’s defined with the `primaryKey` method takes precedence over an `id` property. + + \ No newline at end of file diff --git a/www/apps/book/app/advanced-development/data-models/property-types/page.mdx b/www/apps/book/app/advanced-development/data-models/property-types/page.mdx new file mode 100644 index 0000000000..c4bf73b035 --- /dev/null +++ b/www/apps/book/app/advanced-development/data-models/property-types/page.mdx @@ -0,0 +1,198 @@ +export const metadata = { + title: `${pageNumber} Data Model Property Types`, +} + +# {metadata.title} + +In this chapter, you’ll learn about the types of properties in a data model’s schema. + +## id + +The `id` method defines an automatically generated string ID property. + +For example: + +export const idHighlights = [["4", ".id()", "Define an `id` property."]] + +```ts highlights={idHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + id: model.id(), + // ... +}) + +export default MyCustom +``` + +By default, this property is considered to be the data model’s primary key. + +--- + +## text + +The `text` method defines a string property. + +For example: + +export const textHighlights = [["4", "text", "Define a `text` property."]] + +```ts highlights={textHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + name: model.text(), + // ... +}) + +export default MyCustom +``` + +--- + +## number + +The `number` method defines a number property. + +For example: + +export const numberHighlights = [["4", "number", "Define a `number` property."]] + +```ts highlights={numberHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + age: model.number(), + // ... +}) + +export default MyCustom +``` + +--- + +## bigNumber + +The `bigNumber` method defines a number property that expects large numbers, such as prices. + +For example: + +export const bigNumberHighlights = [["4", "bigNumber", "Define a `bigNumber` property."]] + +```ts highlights={bigNumberHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + price: model.bigNumber(), + // ... +}) + +export default MyCustom +``` + +--- + +## boolean + +The `boolean` method defines a boolean property. + +For example: + +export const booleanHighlights = [["4", "boolean", "Define a `boolean` property."]] + +```ts highlights={booleanHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + hasAccount: model.boolean(), + // ... +}) + +export default MyCustom +``` + +--- + +### enum + +The `enum` method defines a property whose value can only be one of the specified values. + +For example: + +export const enumHighlights = [["4", "enum", "Define a `enum` property."]] + +```ts highlights={enumHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + color: model.enum(["black", "white"]), + // ... +}) + +export default MyCustom +``` + +The `enum` method accepts an array of possible string values. + +--- + +## dateTime + +The `dateTime` method defines a timestamp property. + +For example: + +export const dateTimeHighlights = [["4", "dateTime", "Define a `dateTime` property."]] + +```ts highlights={dateTimeHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + date_of_birth: model.dateTime(), + // ... +}) + +export default MyCustom +``` + +--- + +## json + +The `json` method defines a property whose value is a stringified JSON object. + +For example: + +export const jsonHighlights = [["4", "json", "Define a `json` property."]] + +```ts highlights={jsonHighlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + metadata: model.json(), + // ... +}) + +export default MyCustom +``` + +--- + +## array + +The `array` method defines an array or strings property. + +For example: + +export const arrHightlights = [["4", "array", "Define an `array` property."]] + +```ts highlights={arrHightlights} +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + names: model.array(), + // ... +}) + +export default MyCustom +``` diff --git a/www/apps/book/app/advanced-development/data-models/relationships/page.mdx b/www/apps/book/app/advanced-development/data-models/relationships/page.mdx new file mode 100644 index 0000000000..bb847fc982 --- /dev/null +++ b/www/apps/book/app/advanced-development/data-models/relationships/page.mdx @@ -0,0 +1,190 @@ +export const metadata = { + title: `${pageNumber} Data Model Relationships`, +} + +# {metadata.title} + +In this chapter, you’ll learn how to define relationships between data models in your module. + +## What is a Relationship Property? + +A relationship property is defined using relation methods, such as `hasOne` or `belongsTo`. It represents a relationship between two data models in a module. + +--- + +## One-to-One Relationship + +To define a one-to-one relationship, create relationship properties in the data models using the following methods: + +1. `hasOne`: indicates that the model has one record of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. + +For example: + +export const oneToOneHighlights = [ + ["5", "hasOne", "A user has one email."], + ["10", "belongsTo", "An email belongs to a user."], + ["11", `"email"`, "The relationship's name in the `User` data model."] +] + +```ts highlights={oneToOneHighlights} +import { model } from "@medusajs/utils" + +const User = model.define("user", { + id: model.id(), + email: model.hasOne(() => Email), +}) + +const Email = model.define("email", { + id: model.id(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` + +The `hasOne` and `belongsTo` methods accept a function as a first parameter. The function returns the associated data model. + +The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. + +In the example above, a user has one email, and an email belongs to one user. + +--- + +## One-to-Many Relationship + +To define a one-to-many relationship, create relationship properties in the data models using the following methods: + +1. `hasMany`: indicates that the model has more than one records of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. + +For example: + +export const oneToManyHighlights = [ + ["5", "hasMany", "A store has many products"], + ["10", "belongsTo", "A product has one store."], + ["11", `"products"`, "The relationship's name in the `Store` data model."] +] + +```ts highlights={oneToManyHighlights} +import { model } from "@medusajs/utils" + +const Store = model.define("store", { + id: model.id(), + products: model.hasMany(() => Product), +}) + +const Product = model.define("product", { + id: model.id(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) +``` + +In this example, a store has many products, but a product belongs to one store. + +--- + +## Many-to-Many Relationship + +To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. + +For example: + +export const manyToManyHighlights = [ + ["5", "manyToMany", "An order is associated with many products."], + ["10", "manyToMany", "A product is associated with many orders."] +] + +```ts highlights={manyToManyHighlights} +import { model } from "@medusajs/utils" + +const Order = model.define("order", { + id: model.id(), + products: model.manyToMany(() => Product), +}) + +const Product = model.define("product", { + id: model.id(), + order: model.manyToMany(() => Order), +}) +``` + +In this example, an order is associated with many products, and a product is associated with many orders. + +--- + +## Configure Relationship Property Name + +The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. + +As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. + +For example: + +export const relationNameHighlights = [ + ["6", `"owner"`, "The relationship's name in the `Email` data model."], + ["13", `"email"`, "The relationship's name in the `User` data model."] +] + +```ts highlights={relationNameHighlights} +import { model } from "@medusajs/utils" + +const User = model.define("user", { + id: model.id(), + email: model.hasOne(() => Email, { + mappedBy: "owner", + }), +}) + +const Email = model.define("email", { + id: model.id(), + owner: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` + +In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. + +This is useful if the relationship property’s name is different than that of the associated data model. + +--- + +## Cascades + +When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. + +For example, if a store is deleted, its products should also be deleted. + +The `cascades` method used on a data model configures which child records an operation is cascaded to. + +For example: + +export const highlights = [ + ["8", "", "When a store is deleted, delete its associated products."] +] + +```ts highlights={highlights} +import { model } from "@medusajs/utils" + +const Store = model.define("store", { + id: model.id(), + products: model.hasMany(() => Product), +}) +.cascades({ + delete: ["products"], +}) + +const Product = model.define("product", { + id: model.id(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) +``` + +The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to. + +In the example above, when a store is deleted, its associated products are also deleted. diff --git a/www/apps/book/app/advanced-development/data-models/searchable-property/page.mdx b/www/apps/book/app/advanced-development/data-models/searchable-property/page.mdx new file mode 100644 index 0000000000..55af3a7bd1 --- /dev/null +++ b/www/apps/book/app/advanced-development/data-models/searchable-property/page.mdx @@ -0,0 +1,46 @@ +export const metadata = { + title: `${pageNumber} Searchable Data Model Property`, +} + +# {metadata.title} + +In this chapter, you'll learn what a searchable property is and how to define it. + +## What is a Searchable Property? + +Methods generated by the [service factory](../../modules/service-factory/page.mdx) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters. + +When the `q` filter is passed, the query is applied on searchable properties in a data model. + +--- + +## Define a Searchable Property + +The `searchable` method of the `model` utility indicates that a `text` property is searchable. + +For example: + +```ts +import { model } from "@medusajs/utils" + +const MyCustom = model.define("my_custom", { + name: model.text().searchable(), + // ... +}) + +export default MyCustom +``` + +In this example, the `name` property is searchable. + +### Search Example + +If you pass a `q` filter to the `listMyCustoms` method: + +```ts +const myCustoms = await helloModuleService.listMyCustoms({ + q: "John", +}) +``` + +The `q` filter is applied on the `name` property of the `MyCustom` records. diff --git a/www/apps/book/app/advanced-development/data-models/soft-deletable/page.mdx b/www/apps/book/app/advanced-development/data-models/soft-deletable/page.mdx index a90d3f86e0..0b1eb3f957 100644 --- a/www/apps/book/app/advanced-development/data-models/soft-deletable/page.mdx +++ b/www/apps/book/app/advanced-development/data-models/soft-deletable/page.mdx @@ -1,99 +1,19 @@ export const metadata = { - title: `${pageNumber} Soft-Deletable Models`, + title: `${pageNumber} Soft-Deletable Data Models`, } # {metadata.title} -In this document, you'll learn how to create soft-deletable data models. +In this chapter, you’ll learn about soft-deletable data models. -## What is a Soft-Deletable Model? +## What is a Soft-Deletable Data Model? -A soft-deletable data model is a model whose records aren't actually removed from the database when they're deleted. +A soft-deletable data model is a model that has a `deleted_at` `dateTime` property. -Instead, their `deleted_at` field is set to the date the record was deleted. - -When retrieving or listing records of that data model, records having their `deleted_at` field set aren't retrieved unless the `withDeleted` filter is provided. +When a record of the data model is deleted, this field is set to the current date, marking it as deleted. --- -## How to Create a Soft-Deletable Model? +## Configure Data Model Soft-Deletion -To create a soft-deletable model, first, add the following filter decorator to the data model class: - -```ts title="src/module/hello/models/my-soft-deletable.ts" highlights={[["7"]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" -// other imports... -import { Entity, Filter } from "@mikro-orm/core" -import { DALUtils } from "@medusajs/utils" -import { BaseEntity } from "@medusajs/utils" - -@Entity() -@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -class MySoftDeletable extends BaseEntity { - // ... -} - -export default MySoftDeletable -``` - -Then, add a `deleted_at` field to the data model: - -```ts highlights={[["7"], ["8"]]} -// other imports... -import { Property } from "@mikro-orm/core" - -class MySoftDeletable extends BaseEntity { - // ... - - @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null = null -} -``` - ---- - -## Manage Soft-Deletable Models - -Services extending the service factory have methods to soft delete and restore records for all models specified during its creation. - -### Soft Delete a Record - -For example, to soft delete a `MySoftDeletable` record: - -```ts -await helloModuleService.softDelete([ - "id_123", "id_321", -]) -``` - -The method receives an array of IDs of records to delete. - -### Retrieve Soft-Deleted Records - -The `retrieve`, `list`, and `listAndCount` methods accept as a second parameter a configuration object. - -To retrieve soft-deleted records, set `withDeleted` to `true` in the configuration object passed as a second parameter. - -For example: - -```ts -const deletedRecords = await helloModuleService - .listMySoftDeletables({ - // ... - }, { - withDeleted: true, - }) -``` - -### Restore a Soft-Deleted Record - -To restore a `MySoftDeletable` record: - -```ts -await helloModuleService.restore([ - "id_123", "id_321", -]) -``` - -The method also receives an array of IDs of records to restore. - -If the data model isn't the main data model, its method names are `softDelete` and `restore` suffixed with the plural name of the model. For example, `softDeleteMySoftDeletable`. +By default, all data models have a `deleted_at` property and are considered soft-deletable. diff --git a/www/apps/book/app/advanced-development/modules/container/page.mdx b/www/apps/book/app/advanced-development/modules/container/page.mdx index 923f727790..9e2fb70f08 100644 --- a/www/apps/book/app/advanced-development/modules/container/page.mdx +++ b/www/apps/book/app/advanced-development/modules/container/page.mdx @@ -4,24 +4,17 @@ export const metadata = { # {metadata.title} -In this chapter, you'll learn about the module's container and how to register resources in that container. +In this chapter, you'll learn about the module's container and how to resolve resources in that container. ## Module's Container Each module has a local container only used by the resources of that module. -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. +So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, such as: ---- +- `logger`: A utility to log message in the Medusa application's logs. -## Resources Registered in the Module's Container - -Some resources registered in the module's container are: - -- The module's main service. -- A generated service for each data model in your module. The registration name is the camel-case data model name suffixed by `Service`. For example, `myCustomService`. - -![Example of registered resources in the container](https://res.cloudinary.com/dza7lstvk/image/upload/v1714400573/Medusa%20Book/modules-container_mkcbaq.jpg) +{/* TODO add other relevant resources, such as event bus */} --- @@ -33,23 +26,25 @@ A service's constructor accepts as a first parameter an object used to resolve r For example: -```ts highlights={[["5"], ["12"]]} -import { ModulesSdkTypes } from "@medusajs/types" -import { MyCustom } from "./models/my-custom" +```ts highlights={[["4"], ["10"]]} +import { Logger } from "@medusajs/medusa" type InjectedDependencies = { - myCustomService: ModulesSdkTypes.InternalModuleService + logger: Logger } export default class HelloModuleService { - protected myCustomService_: - ModulesSdkTypes.InternalModuleService - constructor({ myCustomService }: InjectedDependencies) { - this.myCustomService_ = myCustomService + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + + this.logger_.info("[HelloModuleService]: Hello World!") } // ... } + ``` ### Loader @@ -58,16 +53,18 @@ A loader function in a module accepts as a parameter an object having the proper For example: -```ts highlights={[["8"]]} +```ts highlights={[["9"]]} import { LoaderOptions, } from "@medusajs/modules-sdk" +import { Logger } from "@medusajs/medusa" export default function helloWorldLoader({ container, }: LoaderOptions) { - const myCustomService = container.resolve("myCustomService") - - // ... + const logger: Logger = container.resolve("logger") + + logger.info("[helloWorldLoader]: Hello, World!") } + ``` diff --git a/www/apps/book/app/advanced-development/modules/database-operations-in-services/page.mdx b/www/apps/book/app/advanced-development/modules/database-operations-in-services/page.mdx deleted file mode 100644 index 388eccd2a1..0000000000 --- a/www/apps/book/app/advanced-development/modules/database-operations-in-services/page.mdx +++ /dev/null @@ -1,68 +0,0 @@ -export const metadata = { - title: `${pageNumber} Database Operations in Service Methods`, -} - -# {metadata.title} - -In this document, you’ll learn how to implement database operations, such as creating a record, in the main service. - -## Use the Data Model's Generated Service - -To perform database operations on a data model, use the model's generated service in the module's container. - -For example: - -export const highlights = [ - ["13", "", "Inject myCustomService, which is the generated service of the `MyCustom` data model."], - ["22", "", "Add a new field for the generated service of the MyCustom data model."], - ["29", "", "Set the class field to the injected dependency."], - ["35", "create", "Use the `create` method of the generated service."] -] - -```ts title="src/modules/hello/service.ts" highlights={highlights} -// other imports... -import { ModulesSdkTypes } from "@medusajs/types" -import { MyCustom } from "./models/my-custom" - -// ... - -// recommended to define type in another file -type CreateMyCustomDTO = { - name: string -} - -type InjectedDependencies = { - myCustomService: ModulesSdkTypes.InternalModuleService -} - -class HelloModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - // ... - >( - // ... - ) { - protected myCustomService_: ModulesSdkTypes.InternalModuleService - - constructor( - { myCustomService }: InjectedDependencies - ) { - // @ts-ignore - super(...arguments) - this.myCustomService_ = myCustomService - } - - async create( - data: CreateMyCustomDTO - ): Promise { - const myCustom = await this.myCustomService_.create( - data - ) - - return myCustom - } -} -``` - -In the above example, you resolve `myCustomService` in the main service's constructor. The `myCustomService` is the generated service for the `myCustom` data model. - -Then, in the `create` method of the main service, you use `myCustomService`'s `create` method to create the record. diff --git a/www/apps/book/app/advanced-development/modules/link-modules/page.mdx b/www/apps/book/app/advanced-development/modules/link-modules/page.mdx index 934cf2dba3..2c53ab8adb 100644 --- a/www/apps/book/app/advanced-development/modules/link-modules/page.mdx +++ b/www/apps/book/app/advanced-development/modules/link-modules/page.mdx @@ -14,7 +14,7 @@ For example, Medusa has a link module that defines a relationship between the Pr ![Diagram showcasing the link module between the Product and Pricing modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651569/Medusa%20Resources/product-pricing_vlxsiq.jpg) -Link modules provide more flexibility in managing relationships between modules while maintaining module isolation. The Medusa application only creates the link tables when both modules are available. +Link modules create the relationship between modules while maintaining module isolation. The Medusa application only creates the link tables when both modules are available. diff --git a/www/apps/book/app/advanced-development/modules/module-relationships/page.mdx b/www/apps/book/app/advanced-development/modules/module-relationships/page.mdx deleted file mode 100644 index 773c4975e2..0000000000 --- a/www/apps/book/app/advanced-development/modules/module-relationships/page.mdx +++ /dev/null @@ -1,275 +0,0 @@ -import { TypeList, CodeTabs, CodeTab } from "docs-ui" - -export const metadata = { - title: `${pageNumber} Module Relationships`, -} - -# {metadata.title} - -In this document, you’ll learn about creating relationships between modules. - -## What is a Module Relationship? - -A module can have a relationship to another module in the form of a reference. - -The Medusa application resolves these relationships while maintaining isolation between the modules and allowing you to retrieve data across them. - - - -- You want to build relationships between the data models of modules. -- You want to assoaciate more fields with the data model of another module. - - - - - -- You’re building relationships between data models in the same module. Use foreign keys instead. - - - ---- - -## How to Create a Module Relationship? - - - -1. Define a `__joinerConfig` method in the module's main service. -2. Configure module to be queryable. - - - -Consider you’re creating a data model that adds custom fields associated with a product: - -```ts title="src/modules/hello/models/custom-product-data.ts" highlights={[["17"]]} collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { BaseEntity } from "@medusajs/utils" -import { - Entity, - PrimaryKey, - Property, -} from "@mikro-orm/core" - -@Entity() -export class CustomProductData extends BaseEntity { - @PrimaryKey({ columnType: "text" }) - id!: string - - @Property({ columnType: "text" }) - custom_field: string - - @Property({ columnType: "text", nullable: true }) - product_id?: string -} -``` - -The `CustomProductData` data model has a `product_id` field to reference the product it adds custom fields for. - - - -When you add a new data model, make sure to: - -- [Create a migration for it.](../../../basics/data-models/page.mdx#create-a-migration) -- [Add it to the second parameter of the main service's factory function.](../service-factory/page.mdx#abstractModuleServiceFactory-parameters) - - - -### 1. Define `__joinerConfig` Method - -To create a relationship to the `product` data model of the Product Module, create a public `__joinerConfig` method in your main module's service: - -export const relationshipsHighlight = [ - ["39", "serviceName", "The name of the module that this relationship is referencing."], - ["40", "alias", "The alias of the data model you’re referencing in the other module."], - ["41", "primaryKey", "The name of the field you’re referencing in the other module’s data model."], - ["42", "foreignKey", "The name of the field in your data models referencing the other module’s model."], -] - -```ts title="src/modules/hello/service.ts" highlights={relationshipsHighlight} collapsibleLines="1-6" expandButtonLabel="Show Imports" -// other imports... -import { MyCustom } from "./models/custom-product-data" -import { CustomProductData } from "./models/custom-product-data" -import { ModuleJoinerConfig } from "@medusajs/types" -import { Modules } from "@medusajs/modules-sdk" - -class HelloModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - // ... - >( - // ... - ) { - - // ... - - __joinerConfig(): ModuleJoinerConfig { - return { - serviceName: "helloModuleService", - alias: [ - { - name: ["my_custom"], - args: { - entity: MyCustom.name, - }, - }, - { - name: ["custom_product_data"], - args: { - entity: CustomProductData.name, - // Only needed if data model isn't main data model - // of service - methodSuffix: "CustomProductDatas", - }, - }, - ], - relationships: [ - { - serviceName: Modules.PRODUCT, - alias: "product", - primaryKey: "id", - foreignKey: "product_id", - }, - ], - } - } - - // ... -} -``` - -This creates a relationship to the `Product` data model of the Product Module using the alias `product`. The `product_id` fields in your data models are considered references to the `id` field of the `Product` data model. - -#### `__joinerConfig` Return Type - - - -### 2. Adjust Module Configuration - -To use relationships in a module, adjust its configuration object passed to `modules` in `medusa-config.js`: - -export const configHighlights = [ - ["7", "isQueryable", "Enable this property to use relationships in a module."] -] - -```js title="medusa-config.js" highlights={configHighlights} -module.exports = defineConfig({ - // ... - modules: { - helloModuleService: { - // ... - definition: { - isQueryable: true, - }, - }, - }, -}) -``` - -Enabling the `isQueryable` property is required to use relationships in a module. - ---- - -## Reference Inner Data Models - -If the data model you’re referencing isn’t the main data model of the main module service, pass to the relationship definition the `args` property: - -```ts title="src/modules/hello/service.ts" highlights={[["20", "methodSuffix", "The suffix of the referenced data model's methods."]]} -class HelloModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - // ... - >( - // ... - ) { - - // ... - - __joinerConfig(): ModuleJoinerConfig { - return { - // ... - relationships: [ - { - serviceName: Modules.PRODUCT, - primaryKey: "id", - foreignKey: "variant_id", - alias: "variant", - args: { - methodSuffix: "Variants", - }, - }, - ], - } - } - } -``` - -The `args` property’s value is an object accepting a `methodSuffix` property. The `methodSuffix` property’s value is the plural name of the data model. - ---- - -## Querying Module Relationships - -The next chapter explains how to query data across module relationships. diff --git a/www/apps/book/app/advanced-development/modules/options/page.mdx b/www/apps/book/app/advanced-development/modules/options/page.mdx index 5cfff51e2e..a788b01db3 100644 --- a/www/apps/book/app/advanced-development/modules/options/page.mdx +++ b/www/apps/book/app/advanced-development/modules/options/page.mdx @@ -34,7 +34,6 @@ module.exports = defineConfig({ }, }, }) - ``` The `options` property’s value is an object. You can pass any properties you want. @@ -47,36 +46,30 @@ The module’s main service receives the module options as a second parameter. For example: -```ts title="src/modules/hello/service.ts" highlights={[["15"], ["21"], ["25"], ["26"], ["27"]]} -// ... +```ts title="src/modules/hello/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]} +import { MedusaService } from "@medusajs/utils" +import MyCustom from "./models/my-custom" // recommended to define type in another file -type HelloModuleOptions = { +type ModuleOptions = { capitalize?: boolean } -class HelloModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - // ... - >( - // ... - ) { - // ... - protected options_: HelloModuleOptions - - constructor( - { - // ... - }: InjectedDependencies, - protected readonly moduleOptions: HelloModuleOptions - ) { - //... - - this.options_ = moduleOptions || { - capitalize: false, - } +export default class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected options_: ModuleOptions + + constructor({}, options?: ModuleOptions) { + super(...arguments) + + this.options_ = options || { + capitalize: false, } } + + // ... +} ``` --- @@ -93,13 +86,13 @@ import { } from "@medusajs/modules-sdk" // recommended to define type in another file -type HelloModuleOptions = { +type ModuleOptions = { capitalize?: boolean } export default function helloWorldLoader({ options, -}: LoaderOptions) { +}: LoaderOptions) { console.log( "[HELLO MODULE] Just started the Medusa application!", diff --git a/www/apps/book/app/advanced-development/modules/remote-query/page.mdx b/www/apps/book/app/advanced-development/modules/remote-query/page.mdx index e42356fb0a..9d4471cf7e 100644 --- a/www/apps/book/app/advanced-development/modules/remote-query/page.mdx +++ b/www/apps/book/app/advanced-development/modules/remote-query/page.mdx @@ -10,21 +10,43 @@ In this chapter, you’ll learn about the remote query and how to use it to fetc ## What is the Remote Query? -The remote query fetches data across modules and their relationships having their `isQueryable` configuration enabled. It’s a function registered in the Medusa container under the `remoteQuery` key. +The remote query fetches data across modules. It’s a function registered in the Medusa container under the `remoteQuery` key. In your resources, such as API routes or workflows, you can resolve the remote query to fetch data across custom modules and Medusa’s commerce modules. --- -## Example: Query Hello Module +## isQueryable Configuration + +Before you use remote query on your module, you must enable the `isQueryable` configuration of the module. + +For example: + +```js +module.exports = defineConfig({ + // ... + modules: { + helloModuleService: { + resolve: "./modules/hello", + definition: { + isQueryable: true, + }, + }, + }, +}) +``` + +--- + +## Remote Query Example For example, create the route `src/api/store/query/route.ts` with the following content: export const exampleHighlights = [ ["18", "", "Resolve the remote query from the Medusa container."], ["21", "remoteQueryObjectFromString", "Utility function to build the query."], - ["22", "entryPoint", "The alias name of the model you’re querying."], - ["23", "fields", "An array of the data model’s field names to retrieve in the result."], + ["22", "entryPoint", "The name of the data model you're querying."], + ["23", "fields", "An array of the data model’s properties to retrieve in the result."], ["27", "remoteQuery", "Run the query using the remote query."] ] @@ -50,12 +72,12 @@ export async function GET( ) const query = remoteQueryObjectFromString({ - entryPoint: "custom_product_data", - fields: ["id", "custom_field", "product.title"], + entryPoint: "my_custom", + fields: ["id", "test"], }) res.json({ - custom_product_data: await remoteQuery(query), + my_customs: await remoteQuery(query), }) } ``` @@ -64,8 +86,8 @@ In the above example, you resolve `remoteQuery` from the Medusa container. Then, you create a query using the `remoteQueryObjectFromString` utility function imported from `@medusajs/utils`. This function accepts as a parameter an object with the following required properties: -- `entryPoint`: The alias name of the model you’re querying. You defined the alias name in the `__joinerConfig` method of your main service. -- `fields`: An array of the data model’s field names to retrieve in the result. You can also specify fields of a relationship using dot notation. +- `entryPoint`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition. +- `fields`: An array of the data model’s properties to retrieve in the result. You then pass the query to the `remoteQuery` function to retrieve the results. @@ -75,17 +97,17 @@ You then pass the query to the `remoteQuery` function to retrieve the results. ```ts highlights={[["6"], ["7"], ["8"], ["9"]]} const query = remoteQueryObjectFromString({ - entryPoint: "custom_product_data", - fields: ["id", "custom_field", "product.title"], + entryPoint: "my_custom", + fields: ["id", "name"], variables: { filters: { id: [ - "cpd_01HWSVWR4D2XVPQ06DQ8X9K7AX", - "cpd_01HWSVWK3KYHKQEE6QGS2JC3FX", + "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", + "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", ], }, }, -}) +}) const result = await remoteQuery(query) ``` @@ -102,7 +124,7 @@ The `remoteQueryObjectFromString` function accepts a `variables` property. You c { name: "filters", type: "`object`", - description: "The filters to apply on any of the data model's fields." + description: "The filters to apply on any of the data model's properties." } ] }, @@ -116,8 +138,8 @@ The `remoteQueryObjectFromString` function accepts a `variables` property. You c ```ts highlights={[["5"], ["6"], ["7"]]} const query = remoteQueryObjectFromString({ - entryPoint: "custom_product_data", - fields: ["id", "custom_field", "product.title"], + entryPoint: "my_custom", + fields: ["id", "name"], variables: { order: { name: "DESC", @@ -128,12 +150,12 @@ const query = remoteQueryObjectFromString({ const result = await remoteQuery(query) ``` -To sort returned records, pass an `order` property to the `variables` property's value. +To sort returned records, pass an `order` property to `variables`. -The `order` property is an object whose keys are field names, and values are either: +The `order` property is an object whose keys are property names, and values are either: -- `ASC` to sort records by that field in ascending order. -- `DESC` to sort records by that field in descending order. +- `ASC` to sort records by that property in ascending order. +- `DESC` to sort records by that property in descending order. --- @@ -141,8 +163,8 @@ The `order` property is an object whose keys are field names, and values are eit ```ts highlights={[["5", "skip", "The number of records to skip before fetching the results."], ["6", "take", "The number of records to fetch."]]} const query = remoteQueryObjectFromString({ - entryPoint: "custom_product_data", - fields: ["id", "custom_field", "product.title"], + entryPoint: "my_custom", + fields: ["id", "name"], variables: { skip: 0, take: 10, @@ -155,7 +177,7 @@ const { } = await remoteQuery(query) ``` -To paginate the returned records, pass the following properties to the `variables` property's value: +To paginate the returned records, pass the following properties to `variables`: - `skip`: (required to apply pagination) The number of records to skip before fetching the results. - `take`: The number of records to fetch. @@ -215,7 +237,6 @@ The remote query function alternatively accepts a string with GraphQL syntax as MedusaRequest, MedusaResponse, } from "@medusajs/medusa" - import { remoteQueryObjectFromString } from "@medusajs/utils" import { ContainerRegistrationKeys } from "@medusajs/utils" import type { RemoteQueryFunction, @@ -231,20 +252,15 @@ The remote query function alternatively accepts a string with GraphQL syntax as const query = ` query { - custom_product_data { + my_custom { id - custom_field - product { - title - } + name } } ` - const result = await remoteQuery(query) - res.json({ - custom_product_data: result, + my_customs: result, }) } ``` @@ -256,15 +272,12 @@ The remote query function alternatively accepts a string with GraphQL syntax as The `remoteQuery` function accepts as a second parameter an object of variables to reference in the GraphQL query. - ```ts highlights={[["2"], ["3"], ["16"], ["17"], ["18"], ["19"]]} + ```ts highlights={[["2"], ["3"], ["13"], ["14"], ["15"], ["16"]]} const query = ` query($id: ID) { - custom_product_data(id: $id) { + my_custom(id: $id) { id - custom_field - product { - title - } + name } } ` @@ -273,8 +286,8 @@ The remote query function alternatively accepts a string with GraphQL syntax as query, { id: [ - "cpd_01HWSVWR4D2XVPQ06DQ8X9K7AX", - "cpd_01HWSVWK3KYHKQEE6QGS2JC3FX", + "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", + "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", ] } ) @@ -285,22 +298,19 @@ The remote query function alternatively accepts a string with GraphQL syntax as ### Sort Records with GraphQL - To sort the records by a field, pass in the query an `order` argument whose value is an object. The object’s key is the field’s name, and the value is either: + To sort the records by a property, pass in the query an `order` argument whose value is an object. The object’s key is the property’s name, and the value is either: - - `ASC` to sort items by that field in ascending order. - - `DESC` to sort items by that field in descending order. + - `ASC` to sort items by that property in ascending order. + - `DESC` to sort items by that property in descending order. For example: ```ts highlights={[["3"]]} const query = ` query { - custom_product_data(order: {custom_field: DESC}) { + my_custom(order: {name: DESC}) { id - custom_field - product { - title - } + name } } ` @@ -320,12 +330,9 @@ The remote query function alternatively accepts a string with GraphQL syntax as ```ts highlights={[["2"], ["3"]]} const query = ` query($skip: Int, $take: Int) { - custom_product_data(skip: $skip, take: $take) { + my_custom(skip: $skip, take: $take) { id - custom_field - product { - title - } + name } } ` diff --git a/www/apps/book/app/advanced-development/modules/service-factory/page.mdx b/www/apps/book/app/advanced-development/modules/service-factory/page.mdx index d00cd01937..02112f402d 100644 --- a/www/apps/book/app/advanced-development/modules/service-factory/page.mdx +++ b/www/apps/book/app/advanced-development/modules/service-factory/page.mdx @@ -6,11 +6,13 @@ export const metadata = { # {metadata.title} -In this document, you’ll learn about what the service factory is and how to use it to create a service. +In this chapter, you’ll learn about what the service factory is and how to use it. ## What is the Service Factory? -Medusa provides a service factory that your module’s main service can extend. The service factory implements data management methods for your data models. +Medusa provides a service factory that your module’s main service can extend. + +The service factory generates data management methods for your data models, so you don't have to implement them manually. @@ -22,34 +24,41 @@ Medusa provides a service factory that your module’s main service can extend. ## How to Extend the Service Factory? -Medusa provides the service factory as a function your service extends. The function creates and returns a service class with generated data-management methods. +Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods. For example, create the file `src/modules/hello/service.ts` with the following content: -```ts title="src/modules/hello/service.ts" -import { ModulesSdkUtils } from "@medusajs/utils" -import { MyCustom } from "./models/my-custom" +export const highlights = [ + ["4", "MedusaService", "The service factory function."], + ["5", "MyCustom", "The data models to generate data-management methods for."] +] -class HelloModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory( - MyCustom, [] - ) { - // TODO implement custom methods - } +```ts title="src/modules/hello/service.ts" highlights={highlights} +import { MedusaService } from "@medusajs/utils" +import MyCustom from "./models/my-custom" + +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + // TODO implement custom methods +} export default HelloModuleService ``` -### abstractModuleServiceFactory Parameters +### MedusaService Parameters -The `abstractModuleServiceFactory` function accepts two parameters: +The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. -1. The first parameter is the main data model this service is creating methods for. For example, `MyCustom`. -2. The second parameter is an array of data models to generate methods for. If you have an `AnotherCustom` data model, this is where you add it. +In the example above, the `HelloModuleService` now has methods to manage the `MyCustom` data model, such as `createMyCustoms`. ### Generated Methods -The service factory generates the following methods for the main data model: +The service factory generates data-management methods for each of the data models provided in the first parameter. + +The method's names are the operation's name, suffixed by the data model's name. + +For example, the following methods are generated for the code snippet above: @@ -62,7 +71,7 @@ The service factory generates the following methods for the main data model: - `list` + `listMyCustoms` @@ -74,7 +83,7 @@ The service factory generates the following methods for the main data model: - `listAndCount` + `listAndCountMyCustoms` @@ -86,7 +95,7 @@ The service factory generates the following methods for the main data model: - `retrieve` + `retrieveMyCustom` @@ -98,7 +107,31 @@ The service factory generates the following methods for the main data model: - `delete` + `createMyCustoms` + + + + + Create and retrieve records of the data model. + + + + + + + `updateMyCustoms` + + + + + Update and retrieve records of the data model. + + + + + + + `deleteMyCustoms` @@ -110,77 +143,32 @@ The service factory generates the following methods for the main data model: - `softDelete` + `softDeleteMyCustoms` - Soft-deletes a record by an ID or filter. This only applies if the data model has a `deleted_at` field. + Soft-deletes records using an array of IDs or an object of filters. - `restore` + `restoreMyCustoms` - Restores a soft-deleted record by an ID or filter. This only applies if the data model has a `deleted_at` field. + Restores soft-deleted records using an array of IDs or an object of filters.
-The same methods are generated for data models passed in the second parameter of the service factory. The methods' names end with the data model's name. For example, `listAnotherCustom`. + -### Type Arguments +Except for the `retrieve` method, the suffixed data model's name is plural. -For a better development experience and accurate typing of the generated methods, the `abstractModuleServiceFactory` function accepts three type arguments: - -export const typeArgsHighlights = [ - ["25", "InjectedDependencies", "The type of dependencies resolved from the Module's container."], - ["26", "MyCustomDTO", "The expected input/output type of the main data model's generated methods."], - ["27", "AllModelsDTO", "The expected input/output type of the generated methods of every data model."], -] - -```ts title="src/modules/hello/service.ts" highlights={typeArgsHighlights} collapsibleLines="1-22" expandButtonLabel="Show More" -import { ModulesSdkUtils } from "@medusajs/utils" -import { MyCustom } from "./models/my-custom" - -// recommended to define type in another file -type MyCustomDTO = { - id: string - name: string -} - -type InjectedDependencies = { - // TODO add dependencies -} - -type AllModelsDTO = { - MyCustom: { - dto: MyCustomDTO - } -} - -// add other data models in your module here. -const generateMethodsFor = [] - -class HelloModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - InjectedDependencies, - MyCustomDTO, - AllModelsDTO - >(MyCustom, generateMethodsFor) { - // TODO implement custom methods - } - -export default HelloModuleService -``` - -1. The first one is the type of the dependencies to resolve from the module's container. -2. The second one is the expected input and output type of the main data model’s methods. -3. The third type is the expected input and output type of all data models that the service factory generates methods for. + diff --git a/www/apps/book/app/basics/commerce-modules/page.mdx b/www/apps/book/app/basics/commerce-modules/page.mdx index 9fd5fa1a9e..cec0860651 100644 --- a/www/apps/book/app/basics/commerce-modules/page.mdx +++ b/www/apps/book/app/basics/commerce-modules/page.mdx @@ -8,7 +8,9 @@ In this chapter, you'll learn about Medusa's commerce modules. ## What is a Commerce Module? -Medusa provides all its commerce features as separate modules, such as the Product Module, Cart Module, or Order Module. These modules and your custom modules are interchangeable in the Medusa application, making Medusa’s architecture more flexible. +Medusa provides all its commerce features as separate modules, such as the Product or Order modules. + +These modules and your custom modules are interchangeable in the Medusa application, making Medusa’s architecture more flexible. Refer to [this reference](!resources!/commerce-modules) for a full list of commerce modules in Medusa. @@ -47,6 +49,6 @@ When you resolve the `ModuleRegistrationName.PRODUCT` (or `productModuleService` -To resolve the main service of any commerce module, use the `ModuleRegistrationName` enum imported from `@medusajs/modules-sdk` to refer to its registration name in the Medusa container. +To resolve the main service of any commerce module, use the registration name defined in the `ModuleRegistrationName` enum imported from `@medusajs/modules-sdk`. \ No newline at end of file diff --git a/www/apps/book/app/basics/data-models/page.mdx b/www/apps/book/app/basics/data-models/page.mdx index bc1ecd2f33..aca44de7cf 100644 --- a/www/apps/book/app/basics/data-models/page.mdx +++ b/www/apps/book/app/basics/data-models/page.mdx @@ -8,9 +8,7 @@ In this chapter, you’ll learn what data models are and how to create a data mo ## What is a Data Model? -A data model is a class that represents a table in the database. A data model is created in a module. You can then create a service that manages that data model. - -Data models are based on [MikroORM](https://mikro-orm.io/docs/quick-start). So, you can use its decorators, types, and utilities when creating a model. +A data model is a class that represents a table in the database. It's created in a module. --- @@ -20,67 +18,66 @@ Data models are based on [MikroORM](https://mikro-orm.io/docs/quick-start). So, 1. Create data model class in a module. 2. Generate migration for the data model. -3. Add migration scripts to the module's definition. 4. Run migration to add table for data model in the database.
-A data model is a class created in a TypeScript or JavaScript file under a module's `models` directory. +A data model is created in a TypeScript or JavaScript file under a module's `models` directory. It's defined using the `model` utility imported from `@medjusajs/utils`. For example, create the file `src/modules/hello/models/my-custom.ts` with the following content: ```ts title="src/modules/hello/models/my-custom.ts" -import { BaseEntity } from "@medusajs/utils" -import { - Entity, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/utils" -@Entity() -export class MyCustom extends BaseEntity { - @PrimaryKey({ columnType: "text" }) - id!: string +const MyCustom = model.define("my_custom", { + id: model.id(), + name: model.text(), +}) - @Property({ columnType: "text" }) - name: string -} +export default MyCustom ``` -This defines a new data model `MyCustom` with the fields `id` and `name`. Data models extend the `BaseEntity` class imported from `@medusajs/utils`. +You define a data model using the `model`'s `define` method. It accepts two parameters: + +1. The first one is the name of the data model's table in the database. +2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods. + +The example above defines the data model `MyCustom` with the properties `id` and `name`. ### Create a Migration -After creating the data model, you must create a migration that creates a table in your database for this data model. +A migration defines changes to be made in the database, such as create or update tables. -A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It implements an `up` and `down` method, where the `up` method reflects changes on the database, and the `down` method reverts the changes from the database. +So, you must create a migration that creates a table for your data model in the database. -
- MikroORM provides a CLI tool that helps you generate migrations. To use it: +A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: - 1. Create the file `src/modules/hello/mikro-orm.config.dev.ts` with the following content: +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. - ```ts highlights={[["8", "hello", "The module's name."]]} +
+ To generate migrations: + + 1. Create the file `src/modules/hello/migrations-config.ts` with the following content: + + ```ts highlights={[["7", '"medusa-hello"', "Use any database name relevant for your module."]]} + import { defineMikroOrmCliConfig } from "@medusajs/utils" import path from "path" - import { TSMigrationGenerator } from "@medusajs/utils" - import { MyCustom } from "./models/my-custom" + import MyCustom from "./models/my-custom" - module.exports = { - entities: [MyCustom], - schema: "public", - clientUrl: "postgres://postgres@localhost/medusa-hello", - type: "postgresql", + export default defineMikroOrmCliConfig({ + entities: [MyCustom] as any[], + databaseName: "medusa-hello", migrations: { path: path.join(__dirname, "migrations"), - generator: TSMigrationGenerator, }, - } + }) ``` 2. Run the following command in the root directory of your Medusa application: ```bash - npx cross-env MIKRO_ORM_CLI=./src/modules/hello/mikro-orm.config.dev.ts mikro-orm migration:create + npx cross-env MIKRO_ORM_CLI=./src/modules/hello/migrations-config.ts mikro-orm migration:create ``` @@ -89,7 +86,7 @@ A migration is a class created in a TypeScript or JavaScript file under a module - After running the command, a new file is created under the `src/modules/hello/migrations` directory. This file holds `up` and `down` methods that define the actions to execute when running and reverting the migration respectively. + After running the command, a migration file is generated under the `src/modules/hello/migrations` directory.
@@ -98,10 +95,10 @@ For example: ```ts title="src/modules/migrations/Migration20240429090012.ts" import { Migration } from "@mikro-orm/migrations" -export class Migration20240429090012 extends Migration { +export class Migration20240624145652 extends Migration { async up(): Promise { - this.addSql("create table if not exists \"my_custom\" (\"id\" varchar(255) not null, \"name\" text not null, constraint \"my_custom_pkey\" primary key (\"id\"));") + this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));") } async down(): Promise { diff --git a/www/apps/book/app/basics/events-and-subscribers/page.mdx b/www/apps/book/app/basics/events-and-subscribers/page.mdx index 639d81c83b..44c70d4753 100644 --- a/www/apps/book/app/basics/events-and-subscribers/page.mdx +++ b/www/apps/book/app/basics/events-and-subscribers/page.mdx @@ -81,9 +81,9 @@ For example: {/* TODO change how event names are loaded */} export const highlights = [ - ["11"], - ["14", "resolve", "Resolve the `IProductModuleService`."], - ["14", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."] + ["10", "container", "Recieve the Medusa Container in the object parameter."], + ["13", "resolve", "Resolve the Product Module's main service."], + ["13", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."] ] ```ts title="src/subscribers/product-created.ts" highlights={highlights} @@ -111,4 +111,4 @@ export const config: SubscriberConfig = { } ``` -You use the container to resolve the `IProductModuleService`, then log the title of the created product. +You use the container to resolve the Product Module's main service, then log the title of the created product. diff --git a/www/apps/book/app/basics/medusa-container/page.mdx b/www/apps/book/app/basics/medusa-container/page.mdx index 0f1bae432c..ae33f42f62 100644 --- a/www/apps/book/app/basics/medusa-container/page.mdx +++ b/www/apps/book/app/basics/medusa-container/page.mdx @@ -15,7 +15,7 @@ You use the Medusa container to resolve resources, such as services. For example, in a custom API route you can resolve any service registered in the Medusa application using the `scope.resolve` method of the `MedusaRequest` parameter: export const highlights = [ - ["13", "resolve", "Resolve the `IProductModuleService`"], + ["13", "resolve", "Resolve the Product Module's main service."], ["13", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."] ] @@ -41,5 +41,3 @@ export const GET = async ( }) } ``` - -You resolve the `IProductModuleService` and uses it to return the full count of products in the Medusa application. diff --git a/www/apps/book/app/basics/scheduled-jobs/page.mdx b/www/apps/book/app/basics/scheduled-jobs/page.mdx index 387cad1311..2d4cc81602 100644 --- a/www/apps/book/app/basics/scheduled-jobs/page.mdx +++ b/www/apps/book/app/basics/scheduled-jobs/page.mdx @@ -115,7 +115,7 @@ The scheduled job function receives an object parameter that has a `container` p For example: export const highlights = [ - ["12", "resolve", "Resolve the `IProductModuleService`."], + ["12", "resolve", "Resolve the Product Module's main service."], ["12", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."] ] @@ -147,4 +147,4 @@ export const config: ScheduledJobConfig = { } ``` -In the scheduled job function, you resolve the `IProductModuleService` and retrieve the number of products in the store, then log the number in the terminal. \ No newline at end of file +In the scheduled job function, you resolve the Product Module's main service and retrieve the number of products in the store, then log the number in the terminal. \ No newline at end of file diff --git a/www/apps/book/app/basics/workflows/page.mdx b/www/apps/book/app/basics/workflows/page.mdx index de6d3a39ee..c9844f9efa 100644 --- a/www/apps/book/app/basics/workflows/page.mdx +++ b/www/apps/book/app/basics/workflows/page.mdx @@ -228,11 +228,11 @@ Each step in the workflow receives as a second parameter a `context` object. The For example: export const highlights = [ - ["14", "resolve", "Resolve the `IProductModuleService`."], - ["14", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."] + ["15", "resolve", "Resolve the Product Module's main service."], + ["15", "ModuleRegistrationName.PRODUCT", "The resource registration name imported from `@medusajs/modules-sdk`."] ] -```ts title="src/workflows/product-count.ts" highlights={highlights} +```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" import { createStep, StepResponse, @@ -265,4 +265,6 @@ const myWorkflow = createWorkflow( ) export default myWorkflow -``` \ No newline at end of file +``` + +In the step, you resolve the Product Module's main service and use it to retrieve the product count. \ No newline at end of file diff --git a/www/apps/book/app/debugging-and-testing/feature-flags/page.mdx b/www/apps/book/app/debugging-and-testing/_feature-flags/page.mdx similarity index 100% rename from www/apps/book/app/debugging-and-testing/feature-flags/page.mdx rename to www/apps/book/app/debugging-and-testing/_feature-flags/page.mdx diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index 18988ab76f..d60fc7a258 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -106,18 +106,10 @@ export const sidebar = sidebarAttachHrefCommonOptions( path: "/advanced-development/modules/service-factory", title: "Service Factory", }, - { - path: "/advanced-development/modules/database-operations-in-services", - title: "Database Operations", - }, { path: "/advanced-development/modules/options", title: "Module Options", }, - { - path: "/advanced-development/modules/module-relationships", - title: "Module Relationships", - }, { path: "/advanced-development/modules/remote-query", title: "Remote Query", @@ -136,13 +128,37 @@ export const sidebar = sidebarAttachHrefCommonOptions( title: "Data Models", children: [ { - path: "/advanced-development/data-models/common-definitions", - title: "Common Definitions", + path: "/advanced-development/data-models/property-types", + title: "Property Types", + }, + { + path: "/advanced-development/data-models/configure-properties", + title: "Configure Properties", + }, + { + path: "/advanced-development/data-models/primary-key", + title: "Primary Key", + }, + { + path: "/advanced-development/data-models/relationships", + title: "Relationships", + }, + { + path: "/advanced-development/data-models/relationship-cascades", + title: "Relationship Cascades", + }, + { + path: "/advanced-development/data-models/indexes", + title: "Data Model Index", }, { path: "/advanced-development/data-models/soft-deletable", title: "Soft-Deletable Models", }, + { + path: "/advanced-development/data-models/searchable-property", + title: "Searchable Property", + }, ], }, { diff --git a/www/apps/resources/app/_plugins/analytics/segment/page.mdx b/www/apps/resources/app/_plugins/analytics/segment/page.mdx deleted file mode 100644 index 8257f2e3f2..0000000000 --- a/www/apps/resources/app/_plugins/analytics/segment/page.mdx +++ /dev/null @@ -1,196 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Segment Plugin`, -} - -# {metadata.title} - -## Features - -[Segment](https://github.com/medusajs/medusa/tree/master/packages/medusa-plugin-segment) is a powerful Customer Data Platform that allows users to collect, transform, send and archive their customer data. - -Through Segment, you can integrate other third-party services such as: - -- Google Analytics -- Mailchimp -- Zendesk -- Data warehousing for advanced data analytics and segmentation through services like Metabase - -![Diagram illustrating how data goes from Medusa through Segment to other destinations](https://res.cloudinary.com/dza7lstvk/image/upload/v1709135314/Medusa%20Resources/segment-diagram_mye5sc.jpg) - -The Segment plugin in Medusa allows you to track ecommerce events and record them in Segment. Then, you can push these events to other destinations using Segment. - ---- - -## Events That the Segment Plugin Tracks - -The Segment plugin tracks the following events: - -1. `order.placed`: Triggered when an order is placed. -2. `order.shipment_created`: Triggered when a shipment is created for an order. -3. `claim.created`: Triggered when a new claim is created. -4. `order.items_returned`: Triggered when an item in an order is returned. -5. `order.canceled`: Triggered when an order is canceled. -6. `swap.created`: Triggered when a swap is created. -7. `swap.shipment_created`: Triggered when a shipment is created for a swap. -8. `swap.payment_completed`: Triggered when payment for a swap is completed. - - - -Check out the [Event Reference](../../../events-reference/page.mdx) to learn more about these events and their data payloads. - - - ---- - -## Preparations - - - -- [Segment Account](https://app.segment.com/signup/) - - - -### Create a Segment Source - -On your Segment dashboard: - -1. Choose Catalog from the sidebar under Connections. -2. Search for "Node.js" or find "Node.js" under the Sources directory. -3. In the Node.js details page, click on Add Source. -4. This opens a new page to create a Node.js source. Enter the name of the source then click Add Source. -5. On the new source's dashboard, find a "Write Key". You’ll use this key in the next section after you install the Segment plugin in your Medusa application. - -### Optional: Add Destination - -After you create the Segment source, you can add destinations. This is where the data is sent when you send them to Segment. You can add more than one destination. - -To add a destination: - -1. Choose Destinations from the sidebar under Connections. -2. Click on the "Add destination" button. -3. Choose the desired destination, such as Google Universal Analytics or Facebook Pixel. - -The process of integrating each destination is different, so you must follow the steps detailed in Segment for each destination you choose. - ---- - -## Install the Segment Plugin - -To install the Segment plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-segment -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "write_key", "The Segment source's write key."] -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-plugin-segment`, - options: { - write_key: process.env.SEGMENT_WRITE_KEY, - }, - }, -] -``` - -### Segment Plugin Options - - - - - Option - Description - - - - - - - `write_key` - - - - - The Segment source's write key. - - - - -
- -### Environment Variables - -Make sure to add the following environment variables: - -```bash -SEGMENT_WRITE_KEY= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, try triggering one of the [mentioned events earlier in this document](#events-that-the-segment-plugin-tracks). For example, you can place an order either using the [REST APIs](https://docs.medusajs.com/api/store) or using the [Next.js Starter](../../../nextjs-starter/page.mdx). - -After you place an order, on the Segment source that you created, click on the Debugger tab. You should see at least one event triggered for each order you place. If you click on the event, you can see the order details are passed to the event. - -If you added a destination, you can also check your destination to make sure the data is reflected there. - ---- - -## Add Custom Tracking - -The `SegmentService` allows you to track other Medusa events or custom events. - -For example, create the file `src/subscribers/customer.ts` with the following content: - -```ts title="src/subscribers/customer.ts" -import { - type SubscriberConfig, - type SubscriberArgs, - CustomerService, -} from "@medusajs/medusa" - -export default async function handleCustomerCreated({ - data, - container, -}: SubscriberArgs>) { - const segmentService = container.resolve("segmentService") - - const customerData = data - delete customerData["password_hash"] - - segmentService.track({ - event: "Customer Created", - userId: data.id, - properties: customerData, - }) -} - -export const config: SubscriberConfig = { - event: CustomerService.Events.CREATED, -} -``` - -This creates a subscriber that listens to the `CustomerService.Events.CREATED` (`customer.created`) event and sends tracking information to Segment for every customer created. - -The `SegmentService` has a `track` method used to send tracking data to Segment. It accepts an object of data, where the keys `event` and `userId` are required. Instead of `userId`, you can use `anonymousId` to pass an anonymous user ID. - -To pass additional data to Segment, pass them under the `properties` object key. - -The `SegmentService` also has the method `identify` to tie a user to their actions or specific traits. diff --git a/www/apps/resources/app/_plugins/cms/contentful/page.mdx b/www/apps/resources/app/_plugins/cms/contentful/page.mdx deleted file mode 100644 index c7f0c7a008..0000000000 --- a/www/apps/resources/app/_plugins/cms/contentful/page.mdx +++ /dev/null @@ -1,686 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Contentful Plugin`, -} - -# {metadata.title} - -## Features - -[Contentful](https://www.contentful.com/) is a headless CMS service that allows developers to integrate rich CMS functionalities into any platform. - -By integrating Contentful to Medusa, you can benefit from powerful features in your ecommerce store such as: - -- Rich CMS details for product. -- Easy-to-use interface to manage content for static content and pages. -- Localization for product and storefront content. -- Two-way sync between Contentful and Medusa. - ---- - -## Install the Contentful Plugin - - - -- [Contentful account with a space](https://www.contentful.com/sign-up/). -- An Event Module installed in the Medusa application, such as the [Redis Event Module](../../../architectural-modules/event/redis/page.mdx). - - - -To install the Contentful plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-contentful -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "space_id", "The Contentful space's ID."], - ["7", "access_token", "The personal access token for content management."], - ["8", "environment", "The Contentful environment."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-plugin-contentful`, - options: { - space_id: process.env.CONTENTFUL_SPACE_ID, - access_token: process.env.CONTENTFUL_ACCESS_TOKEN, - environment: process.env.CONTENTFUL_ENV, - }, - }, -] -``` - -### Contentful Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `space_id` - - - - - A string indicating the ID of your [Contentful space](https://www.contentful.com/help/find-space-id/). - - - - - Yes - - - - - \- - - - - - - - `access_token` - - - - - A string indicating [the personal access token for content management](https://www.contentful.com/help/personal-access-tokens/#how-to-get-a-personal-access-token-the-web-app). - - - - - Yes - - - - - \- - - - - - - - `environment` - - - - - A string indicating the [Contentful environment](https://www.contentful.com/developers/docs/concepts/multiple-environments/). Typically, its value should be `master`. - - - - - Yes - - - - - \- - - - - - - - `ignore_threshold` - - - - - The number of seconds to wait before re-syncing a specific record. - - - - - No - - - - - `2` - - - - - - - `custom__fields` - - - - - An object that allows you to map fields in Medusa to [custom field names](#custom-field-mapping). - - - - - No - - - - - \- - - - - -
- -### Environment Variables - -Make sure to add the following environment variables. - -```bash -CONTENTFUL_SPACE_ID= -CONTENTFUL_ACCESS_TOKEN= -CONTENTFUL_ENV=master -``` - -### Custom Field Mapping - -When the plugin syncs data between Contentful and Medusa, it expects a set of fields to be defined in the respective content models in Contentful. If you use different names to define those fields in Contentful, specify them in the `custom__fields` option mentioned earlier, where `` is the name of the content model. - -For example, to change the name of the product’s `title` field, pass the following option to the plugin: - -```js title="medusa-config.js" highlights={[["9"], ["10"], ["11"]]} -const plugins = [ - // ... - { - resolve: `medusa-plugin-contentful`, - options: { - space_id: process.env.CONTENTFUL_SPACE_ID, - access_token: process.env.CONTENTFUL_ACCESS_TOKEN, - environment: process.env.CONTENTFUL_ENV, - custom_product_fields: { - title: "name", - }, - }, - }, -] -``` - -The rest of this section includes the field names you can customize using this option for each content model type. - -
- - - `title` - - `subtitle` - - `description` - - `variants` - - `options` - - `medusaId` - - `type` - - `collection` - - `tags` - - `handle` - -
- -
- - - `title` - - `sku` - - `prices` - - `options` - - `medusaId` - -
- -
- - - `name` - - `countries` - - `paymentProviders` - - `fulfillmentProviders` - - `medusaId` - -
- -
- - - `title` - - `medusaId` - -
- -
- - - `name` - - `medusaId` - -
- -### Migrate Content Models - -In your Contentful space, you must have content models for Medusa entities such as products and regions. - -You can either create the content models manually, or create a loader in the Medusa application that migrates these content models into Contentful. - -This section includes migration scripts for Medusa’s data models that are relevant for Contentful. - -Before creating the migration scripts, run the following command in the root of your Medusa backend to install Contentful’s migration SDK: - -```bash npm2yarn -npm install --save-dev contentful-migration -``` - -
- - Create the file `src/loaders/contentful-migrations/product.ts` with the following content: - - ```ts title="src/loaders/contentful-migrations/product.ts" - import Migration, { - MigrationContext, - } from "contentful-migration" - - export function productMigration( - migration: Migration, - context?: MigrationContext - ) { - const product = migration - .createContentType("product") - .name("Product") - .displayField("title") - - product - .createField("title") - .name("Title") - .type("Symbol") - .required(true) - product - .createField("subtitle") - .name("Subtitle") - .type("Symbol") - product - .createField("handle") - .name("Handle") - .type("Symbol") - product - .createField("thumbnail") - .name("Thumbnail") - .type("Link") - .linkType("Asset") - product - .createField("description") - .name("Description") - .type("Text") - product - .createField("options") - .name("Options") - .type("Object") - product - .createField("tags") - .name("Tags") - .type("Object") - product - .createField("collection") - .name("Collection") - .type("Symbol") - product - .createField("type") - .name("Type") - .type("Symbol") - product - .createField("variants") - .name("Variants") - .type("Array") - .items({ - type: "Link", - linkType: "Entry", - validations: [ - { - linkContentType: ["productVariant"], - }, - ], - }) - product - .createField("medusaId") - .name("Medusa ID") - .type("Symbol") - } - ``` - -
- -
- - Create the file `src/loaders/contentful-migrations/product-variant.ts` with the following content: - - ```ts title="src/loaders/contentful-migrations/product-variant.ts" - import Migration, { - MigrationContext, - } from "contentful-migration" - - export function productVariantMigration( - migration: Migration, - context?: MigrationContext - ) { - const productVariant = migration - .createContentType("productVariant") - .name("Product Variant") - .displayField("title") - - productVariant - .createField("title") - .name("Title") - .type("Symbol") - .required(true) - productVariant - .createField("sku") - .name("SKU") - .type("Symbol") - productVariant - .createField("options") - .name("Options") - .type("Object") - productVariant - .createField("prices") - .name("Prices") - .type("Object") - productVariant - .createField("medusaId") - .name("Medusa ID") - .type("Symbol") - } - ``` - -
- -
- - Create the file `src/loaders/contentful-migrations/product-collection.ts` with the following content: - - ```ts title="src/loaders/contentful-migrations/product-collection.ts" - import Migration, { - MigrationContext, - } from "contentful-migration" - - export function productCollectionMigration( - migration: Migration, - context?: MigrationContext - ) { - const collection = migration - .createContentType("collection") - .name("Product Collection") - .displayField("title") - - collection - .createField("title") - .name("Title") - .type("Symbol") - .required(true) - collection - .createField("medusaId") - .name("Medusa ID") - .type("Symbol") - } - ``` - -
- -
- - Create the file `src/loaders/contentful-migrations/product-type.ts` with the following content: - - ```ts title="src/loaders/contentful-migrations/product-type.ts" - import Migration, { - MigrationContext, - } from "contentful-migration" - - export function productTypeMigration( - migration: Migration, - context?: MigrationContext - ) { - const collection = migration - .createContentType("productType") - .name("Product Type") - .displayField("title") - - collection - .createField("title") - .name("Title") - .type("Symbol") - .required(true) - collection - .createField("medusaId") - .name("Medusa ID") - .type("Symbol") - } - ``` - -
- -
- - Create the file `src/loaders/contentful-migrations/region.ts` with the following content: - - ```ts title="src/loaders/contentful-migrations/region.ts" - import Migration, { - MigrationContext, - } from "contentful-migration" - - export function regionMigration( - migration: Migration, - context?: MigrationContext - ) { - const region = migration - .createContentType("region") - .name("Region") - .displayField("name") - - region - .createField("name") - .name("Name") - .type("Symbol") - .required(true) - region - .createField("countries") - .name("Options") - .type("Object") - region - .createField("paymentProviders") - .name("Payment Providers") - .type("Object") - region - .createField("fulfillmentProviders") - .name("Fulfillment Providers") - .type("Object") - region - .createField("currencyCode") - .name("Currency Code") - .type("Symbol") - region - .createField("medusaId") - .name("Medusa ID") - .type("Symbol") - } - ``` - -
- -Finally, create a loader at `src/loaders/index.ts` with the following content: - -```ts title="src/loaders/index.ts" -import { - ConfigModule, - StoreService, - MedusaContainer, -} from "@medusajs/medusa" -import { runMigration } from "contentful-migration" -import { - productMigration, -} from "./contentful-migrations/product" -import { - productVariantMigration, -} from "./contentful-migrations/product-variant" -import { - productCollectionMigration, -} from "./contentful-migrations/product-collection" -import { - productTypeMigration, -} from "./contentful-migrations/product-type" -import { - regionMigration, -} from "./contentful-migrations/region" - -type ContentfulPluginType = { - resolve: string - options: { - space_id: string - access_token: string - environment: string - } -} - -export default async ( - container: MedusaContainer, - config: ConfigModule -): Promise => { - // ensure that migration only runs once - const storeService = container.resolve( - "storeService" - ) - const store = await storeService.retrieve() - - if (store.metadata?.ran_contentful_migrations) { - return - } - - console.info("Running contentful migrations...") - - // load Contentful options - const contentfulPlugin = config.plugins - .find((plugin) => - typeof plugin === "object" && - plugin.resolve === "medusa-plugin-contentful" - ) as ContentfulPluginType - - if (!contentfulPlugin) { - console.log( - "Didn't find Contentful plugin. Aborting migration..." - ) - return - } - - const options = { - spaceId: contentfulPlugin.options.space_id, - accessToken: contentfulPlugin.options.access_token, - environment: contentfulPlugin.options.environment, - yes: true, - } - - const migrationFunctions = [ - { - name: "Product", - function: productMigration, - }, - { - name: "Product Variant", - function: productVariantMigration, - }, - { - name: "Product Collection", - function: productCollectionMigration, - }, - { - name: "Product Type", - function: productTypeMigration, - }, - { - name: "Region", - function: regionMigration, - }, - ] - - await Promise.all( - migrationFunctions.map(async (migrationFunction) => { - console.info(`Migrating ${ - migrationFunction.name - } component...`) - try { - await runMigration({ - ...options, - migrationFunction: migrationFunction.function, - }) - console.info(`Finished migrating ${ - migrationFunction.name - } component`) - } catch (e) { - if ( - typeof e === "object" && "errors" in e && - Array.isArray(e.errors) && - e.errors.length > 0 && - e.errors[0].type === "Invalid Action" && - e.errors[0].message.includes("already exists") - ) { - console.info(`${ - migrationFunction.name - } already exists. Skipping its migration.`) - } else { - throw new Error(e) - } - } - }) - ) - - await storeService.update({ - metadata: { - ran_contentful_migrations: true, - }, - }) - - console.info("Finished contentful migrations") -} -``` - -Notice that in the script you store a flag in the default store’s `metadata` attribute to ensure these migrations only run once. - -### Setup Webhooks - -As mentioned in the introduction, this plugin supports two-way sync. A subscriber in the plugin listens to changes in the data, such as adding a new product, and syncs the data with Contentful. - -To update the Medusa application when changes occur in Contentful, you must configure webhooks settings in Contentful. - - - -For webhooks to work, your Medusa application must be deployed and accessible publicly. - - - -To do that: - -1. On your Contentful Space Dashboard, click on Settings from the navigation bar, then choose Webhooks. -2. Click on the Add Webhook button. -3. In the form, enter a name for the webhook. -4. In the URL field, choose the method `POST` and in the input next to it enter the URL `/hooks/contentful` where `` is the URL of your deployed Medusa application. -5. Scroll down to find the Content Type select field. Choose `application/json` as its value. -6. You can leave the rest of the fields the same and click on the Save button. - ---- - -## Test the Plugin - -Run the following command to start your Medusa application and test the plugin: - -```bash npm2yarn -npm run dev -``` - -If you created migration scripts, they’ll run when the Medusa application starts and migrate your content models to Contentful. You can go to your space’s dashboard to confirm they’ve been created. - -After that, try the sync functionality by creating or updating products in the Medusa application. If you’ve also setup webhooks, you can test out the sync from Contentful to Medusa. diff --git a/www/apps/resources/app/_plugins/cms/strapi/page.mdx b/www/apps/resources/app/_plugins/cms/strapi/page.mdx deleted file mode 100644 index 72d18b9fc6..0000000000 --- a/www/apps/resources/app/_plugins/cms/strapi/page.mdx +++ /dev/null @@ -1,229 +0,0 @@ -export const metadata = { - title: `Strapi Plugin`, -} - -# {metadata.title} - - - -This plugin is a [community plugin](https://github.com/SGFGOV/medusa-strapi-repo) and is not managed by the official Medusa team. It supports v4 of Strapi. If you run into any issues, please refer to the [plugin's repository](https://github.com/SGFGOV/medusa-strapi-repo). - - - -## Features - -[Strapi](https://strapi.io/) is an open source headless CMS service that allows developers to have complete control over their content models. It can be integrated into many other frameworks, including Medusa. - -By integrating Strapi into Medusa, you can benefit from powerful features in your ecommerce store, such as: - -- Rich CMS details for product. -- Easy-to-use interface to manage content for static content and pages. -- Localization for product and storefront content. -- Two-way sync between Strapi and Medusa. - ---- - -## Preparations - - - -- A [PostgreSQL database](https://www.postgresql.org/docs/current/sql-createdatabase.html) for Strapi. - - - -In this section, you’ll setup a Strapi project with a Medusa plugin installed. To do that: - -1. Clone the Strapi project repository: - -```bash -git clone https://github.com/SGFGOV/medusa-strapi-repo.git -``` - -2. Change to the `medusa-strapi-repo/packages/medusa-strapi` directory. -3. Copy the `.env.test` file to a new `.env` file. - -### Change Strapi Environment Variables - -In the `.env` file, change the following environment variables: - -```bash -# IMPORTANT: Change supersecret with random and unique strings -APP_KEYS=supersecret -API_TOKEN_SALT=supersecret -ADMIN_JWT_SECRET=supersecret -JWT_SECRET=supersecret - -MEDUSA_STRAPI_SECRET=supersecret - -MEDUSA_BACKEND_URL=http://localhost:9000 -MEDUSA_BACKEND_ADMIN=http://localhost:7001 - -SUPERUSER_EMAIL=support@medusa-commerce.com -SUPERUSER_USERNAME=SuperUser -SUPERUSER_PASSWORD=MedusaStrapi1 - -DATABASE_HOST=localhost -DATABASE_PORT=5432 -DATABASE_NAME=postgres_strapi -DATABASE_USERNAME=postgres -DATABASE_PASSWORD= -DATABASE_SSL=false -DATABASE_SCHEMA=public -``` - -1. Change `APP_KEYS`, `API_TOKEN_SALT`, `JWT_SECRET`, and `ADMIN_JWT_SECRET` to a random and unique string. These keys are used by Strapi to sign session cookies, generate API tokens, and more. -2. Change `MEDUSA_STRAPI_SECRET` to a random unique string. The value of this environment variable is used later in your Medusa configurations. -3. Change `MEDUSA_BACKEND_URL` to the URL of your Medusa backend. If you’re running it locally, it should be `http://localhost:9000`. -4. Change `MEDUSA_BACKEND_ADMIN` to the URL of your Medusa Admin. If you’re running it locally, it should be `http://localhost:7001`. -5. Change the following environment variables to define the Strapi super user: - 1. `SUPERUSER_EMAIL`: the super user’s email. By default, it’s `support@medusa-commerce.com`. - 2. `SUPERUSER_USERNAME`: the super user’s username. By default, it’s `SuperUser`. - 3. `SUPERUSER_PASSWORD`: the super user’s password. By default, it’s `MedusaStrapi1`. - 4. `SUPERUSER_FIRSTNAME`: the super user’s first name. By default, it’s `Medusa`. - 5. `SUPERUSER_LASTNAME`: the super user’s last name. By default, it’s `Commerce`. -6. Change the database environment variables based on your database configurations. All database environment variables start with `DATABASE_`. -7. You can optionally configure other services, such as S3 or MeiliSearch, as explained [here](https://github.com/SGFGOV/medusa-strapi-repo/tree/development/packages/medusa-strapi#media-bucket). - -### Build Packages - -Once you’re done, install and build packages in the root `medusa-strapi-repo` directory: - -```bash npm2yarn -# Install packages -npm install -# Build packages -npm run build -``` - ---- - -## Install the Strapi Plugin in Medusa - - - -- An Event Module installed in the Medusa application, such as the [Redis Event Module](../../../architectural-modules/event/redis/page.mdx). - - - -To install the Strapi plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-strapi-ts -``` - -Next, add the plugin to the `plugins` array in `medusa-config.js`: - -```js title="medusa-config.js" -const plugins = [ - // ... - { - resolve: "medusa-plugin-strapi-ts", - options: { - strapi_protocol: process.env.STRAPI_PROTOCOL, - strapi_host: process.env.STRAPI_SERVER_HOSTNAME, - strapi_port: process.env.STRAPI_PORT, - strapi_secret: process.env.STRAPI_SECRET, - strapi_default_user: { - username: process.env.STRAPI_MEDUSA_USER, - password: process.env.STRAPI_MEDUSA_PASSWORD, - email: process.env.STRAPI_MEDUSA_EMAIL, - confirmed: true, - blocked: false, - provider: "local", - }, - strapi_admin: { - username: process.env.STRAPI_SUPER_USERNAME, - password: process.env.STRAPI_SUPER_PASSWORD, - email: process.env.STRAPI_SUPER_USER_EMAIL, - }, - auto_start: true, - }, - }, -] -``` - -### Strapi Plugin Options - -1. `strapi_protocol`: The protocol of the Strapi server. If running locally, it should be `http`. Otherwise, it should be `https`. -2. `strapi_host`: the domain of the Strapi server. If running locally, use `127.0.0.1`. -3. `strapi_port`: the port that the Strapi server is running on, if any. If running locally, use `1337`. -4. `strapi_secret`: the same secret used for the `MEDUSA_STRAPI_SECRET` environment variable in the Strapi project. -5. `strapi_default_user`: The details of an existing user or a user to create in the Strapi backend that is used to update data in Strapi. It’s an object accepting the following properties: - 1. `username`: The user’s username. - 2. `password`: The user’s password. - 3. `email`: The user’s email. - 4. `confirmed`: Whether the user is confirmed. - 5. `blocked`: Whether the user is blocked. - 6. `provider`: The name of the authentication provider. -6. `strapi_admin`: The details of the super admin. The super admin is only used to create the default user if it doesn’t exist. It’s an object accepting the following properties: - 1. `username`: the super admin’s username. Its value is the same as that of the `SUPERUSER_USERNAME` environment variable in the Strapi project. - 2. `password`: the super admin’s password. Its value is the same as that of the `SUPERUSER_PASSWORD` environment variable in the Strapi project. - 3. `email`: the super admin’s email. Its value is the same as that of the `SUPERUSER_EMAIL` environment variable in the Strapi project. -7. `auto_start`: Whether to initialize the Strapi connection when Medusa starts. Disabling this may cause issues when syncing data from Medusa to Strapi. - -Refer to the [plugin’s README](https://github.com/SGFGOV/medusa-strapi-repo/blob/development/packages/medusa-plugin-strapi-ts/README.md) for more options. - -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -STRAPI_PROTOCOL=http -STRAPI_SERVER_HOSTNAME=127.0.0.1 -STRAPI_PORT=1337 -STRAPI_SECRET=supersecret - -STRAPI_MEDUSA_USER=medusa -STRAPI_MEDUSA_PASSWORD=supersecret -STRAPI_MEDUSA_EMAIL=admin@medusa-test.com - -STRAPI_SUPER_USERNAME=SuperUser -STRAPI_SUPER_PASSWORD=MedusaStrapi1 -STRAPI_SUPER_USER_EMAIL=support@medusa-commerce.com -``` - ---- - -## Test the Plugin - -To test the integration between Medusa and Strapi, first, start the Strapi server by running the following command in the `medusa-strapi-repo/packages/medusa-strapi` directory: - -```bash title="medusa-strapi-repo/packages/medusa-strapi" npm2yarn -npm run develop -``` - -Then, start the Medusa application: - -```bash title="Medusa Backend" npm2yarn -npx medusa develop -``` - -If the connection to Strapi is successful, you’ll find the following message logged in your Medusa application with no errors: - -```bash -info: Checking Strapi Health ,data: -debug: check-url: http://127.0.0.1:1337/_health ,data: -info: Strapi Subscriber Initialized -``` - -### Synced Entities - -The Medusa and Strapi plugins support syncing the following Medusa data models: - -- `Region` -- `Product` -- `ProductVariant` -- `ProductCollection` -- `ProductCategory` - -### Two-Way Syncing - -To test syncing data from Medusa to Strapi, try creating or updating a product either using the Medusa Admin or the [REST APIs](https://docs.medusajs.com/api/admin#products_postproducts). This triggers the associated event in Medusa, which makes the updates in Strapi. - - - -Data is only synced to Strapi once you create or update them. So, if you have products in your Medusa application from before integrating Strapi, they won’t be available by default in Strapi. You’ll have to make updates to them, which triggers the update in Strapi. - - - -To test syncing data from Strapi to Medusa, try updating one of the products in the Strapi dashboard. If you check the product’s details in Medusa, they’re updated as expected. diff --git a/www/apps/resources/app/_plugins/erp/brightpearl/page.mdx b/www/apps/resources/app/_plugins/erp/brightpearl/page.mdx deleted file mode 100644 index 41d7de1ca1..0000000000 --- a/www/apps/resources/app/_plugins/erp/brightpearl/page.mdx +++ /dev/null @@ -1,487 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Brightpearl Plugin`, -} - -# {metadata.title} - -## Features - -[Brightpearl](https://www.brightpearl.com/) is a Retail Operations Platform. It can be integrated to a business's different sales channels to provide features related to inventory management, automation, analytics and reporting, and more. - -Medusa provides an official Brightpearl plugin with the following features: - -- Send and sync orders with Brightpearl. -- Listen for inventory and stock movements in Brightpearl. -- Handle order returns through Brightpearl. - ---- - -## Install the Brightpearl Plugin - - - -- [Brightpearl account](https://www.brightpearl.com/) - - - -To install the Brightpearl plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-brightpearl -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "account", "The Brightpearl account ID."], - ["7", "backend_url", "The URL of the Medusa application."], - ["8", "channel_id", "The ID of the channel to map sales and credits to."], - ["9", "event_owner", "The ID of the contact used when sending the Goods-Out Note Event."], - ["10", "warehouse", "The ID of the warehouse to allocate order items' inventory from."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-plugin-brightpearl`, - options: { - account: process.env.BRIGHTPEARL_ACCOUNT, - backend_url: process.env.BRIGHTPEARL_BACKEND_URL, - channel_id: process.env.BRIGHTPEARL_CHANNEL_ID, - event_owner: process.env.BRIGHTPEARL_EVENT_OWNER, - warehouse: process.env.BRIGHTPEARL_WAREHOUSE, - }, - }, -] -``` - -### Brightpearl Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `account` - - - - - a string indicating your [Brightpearl account ID](https://help.brightpearl.com/s/article/360028541892#:~:text=Your%20account%20ID%20can%20be,your%20email%20address%20and%20password). - - - - - Yes - - - - - \- - - - - - - - `backend_url` - - - - - A string indicating the URL of your Medusa application. This is useful for webhooks. - - - - - Yes - - - - - \- - - - - - - - `channel_id` - - - - - A string indicating the ID of the channel to map sales and credits to. - - - - - Yes - - - - - \- - - - - - - - `event_owner` - - - - - A string indicating the ID of the contact used when sending the [Goods-Out Note Event](https://api-docs.brightpearl.com/warehouse/goods-out-note%20event/post.html). - - - - - Yes - - - - - \- - - - - - - - `warehouse` - - - - - A string indicating the ID of the warehouse to allocate order items' inventory from. - - - - - Yes - - - - - \- - - - - - - - `default_status_id` - - - - - A string indicating the ID of the status to assign new orders. This value will also be used on - swaps or claims if their respective options, `swap_status_id` and `claim_status_id`, are not provided. - - - - - No - - - - - `3` - - - - - - - `swap_status_id` - - - - - A string indicating the ID of the status to assign new swaps. - - - - - No - - - - - Value of `default_status_id`. - - - - - - - `claim_status_id` - - - - - A string indicating the ID of the status to assign new claims. - - - - - No - - - - - Value of `default_status_id`. - - - - - - - `payment_method_code` - - - - - A string indicating the payment method code to register payments with. - - - - - No - - - - - `1220` - - - - - - - `sales_account_code` - - - - - A string indicating the nominal code to assign line items to. - - - - - No - - - - - `4000` - - - - - - - `shipping_account_code` - - - - - A string indicating the nominal code to assign shipping lines to. - - - - - No - - - - - `4040` - - - - - - - `discount_account_code` - - - - - A string indicating the nominal code to use for discount-type refunds. - - - - - No - - - - - \- - - - - - - - `gift_card_account_code` - - - - - A string indicating the nominal code to use for gift card products and redeems. - - - - - No - - - - - `4000` - - - - - - - `inventory_sync_cron` - - - - - A string indicating a cron pattern that should be used to create a scheduled job - for syncing inventory. If not provided, the scheduled job will not be created. - - - - - No - - - - - \- - - - - - - - `cost_price_list` - - - - - A string indicating the ID of the price list to assign to created claims. - - - - - No - - - - - `1` - - - - - - - `base_currency` - - - - - A string indicating the ISO 3 character code of the currency to assign to created claims. - - - - - No - - - - - `EUR` - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -BRIGHTPEARL_ACCOUNT= -BRIGHTPEARL_CHANNEL_ID= -BRIGHTPEARL_BACKEND_URL= -BRIGHTPEARL_EVENT_OWNER= -BRIGHTPEARL_WAREHOUSE= -BRIGHTPEARL_DEFAULT_STATUS_ID= -BRIGHTPEARL_SWAP_STATUS_ID= -BRIGHTPEARL_CLAIM_STATUS_ID= -BRIGHTPEARL_PAYMENT_METHOD_CODE= -BRIGHTPEARL_SALES_ACCOUNT_CODE= -BRIGHTPEARL_SHIPPING_ACCOUNT_CODE= -BRIGHTPEARL_DISCOUNT_ACCOUNT_CODE= -BRIGHTPEARL_GIFT_CARD_ACCOUNT_CODE= -BRIGHTPEARL_INVENTORY_SYNC_CRON= -BRIGHTPEARL_COST_PRICE_LIST= -BRIGHTPEARL_BASE_CURRENCY= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, place an order either using a [storefront](../../../nextjs-starter/page.mdx) or the [Store REST APIs](https://docs.medusajs.com/api/store). The order should appear on Brightpearl. - ---- - -## How the Plugin Works - -### OAuth - -The plugin registers an OAuth app in Medusa allowing installation at `/a/settings/apps`, where `` is the URL of your Medusa application. - -The OAuth tokens are refreshed every hour to prevent unauthorized requests. - -### Orders and Fulfillments - -When an order is created in the Medusa application, it'll automatically be sent to Brightpearl and allocated there. Once allocated, it is up to Brightpearl to figure out how the order is to be fulfilled. - -The plugin listens for Goods-Out notes and tries to map each of these to a Medusa order. If the matching succeeds, the Medusa application sends the order to the fulfillment provider associated with the shipping method selected by the Customer. - -### Order Returns - -When line items in an order are returned, the plugin will generate a sales credit in Brightpearl. - -### Products - -The plugin doesn't automatically create products in Medusa, but listens for inventory changes in Brightpearl. Then, the plugin updates each product variant to reflect the inventory quantity listed in Brightpearl, thereby ensuring that the inventory levels in Medusa are always in sync with Brightpearl. diff --git a/www/apps/resources/app/_plugins/fulfillment/manual/page.mdx b/www/apps/resources/app/_plugins/fulfillment/manual/page.mdx deleted file mode 100644 index c1022f25b8..0000000000 --- a/www/apps/resources/app/_plugins/fulfillment/manual/page.mdx +++ /dev/null @@ -1,48 +0,0 @@ -export const metadata = { - title: `Manual Fulfillment Plugin`, -} - -# {metadata.title} - -## Features - -The manual fulfillment plugin is a minimal plugin that allows merchants to handle fulfillments manually. This plugin is installed by default in your Medusa application. - -The manual fulfillment plugin is similar to a cash-on-delivery (COD) fulfillment plugin. While the merchant can use shipping and fulfillment functionalities, they only change data in the database. The merchant has to handle the actual fulfillment of the order manually. - ---- - -## Install the Manual Fulfillment Plugin - -To install the Manual Fulfillment plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-fulfillment-manual -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -```js title="medusa-config.js" -const plugins = [ - // ... - { - resolve: `medusa-fulfillment-manual`, - }, -] -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, you must enable the Manual Fulfillment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers), or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionfulfillmentproviders). - -After enabling the provider, you must add shipping options for that provider. You can also do that using either the [Medusa Admin](!user-guide!/settings/regions/shipping-options) or the [Admin API Routes](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptions). - -Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). You can use the shipping options you created for the fulfillment provider. diff --git a/www/apps/resources/app/_plugins/fulfillment/webshipper/page.mdx b/www/apps/resources/app/_plugins/fulfillment/webshipper/page.mdx deleted file mode 100644 index e4d9c3385c..0000000000 --- a/www/apps/resources/app/_plugins/fulfillment/webshipper/page.mdx +++ /dev/null @@ -1,333 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Webshipper Plugin`, -} - -# {metadata.title} - -## Features - -[Webshipper](https://webshipper.com/) is a service that allows merchants to connect to multiple carriers through a single Webshipper account. Developers can then integrate webshipper with ecommerce store like Medusa to handle shipping and fulfillment. - -Medusa provides an official plugin that allows you to integrate Webshipper in your store. When integrated, you can provide customers with Webshippers' shipping options on checkout, and process and handle fulfillment and shipments of orders through Webshipper. - ---- - -## Install the Webshipper Plugin - - - -- [Webshipper account](https://webshipper.com/) - - - -To install the Webshipper plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-fulfillment-webshipper -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "account", "The Webshipper account name."], - ["7", "api_token", "The Webshipper API token."], - ["8", "order_channel_id", "The ID of the order channel to retrieve shipping rates from."], - ["10", "webhook_secret", "The secret used to sign the HMAC in webhooks."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-fulfillment-webshipper`, - options: { - account: process.env.WEBSHIPPER_ACCOUNT, - api_token: process.env.WEBSHIPPER_API_TOKEN, - order_channel_id: - process.env.WEBSHIPPER_ORDER_CHANNEL_ID, - webhook_secret: process.env.WEBSHIPPER_WEBHOOK_SECRET, - }, - }, -] -``` - -### Webshipper Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `account` - - - - - A string indicating your account name. It's in the first part of the URL you use when accessing the Webshipper UI which has the format `https://.webshipper.io`. - - - - - Yes - - - - - \- - - - - - - - `api_token` - - - - - A string indicating your API token. You can create it from the Webshipper UI under Settings > Access and tokens. - - - - - Yes - - - - - \- - - - - - - - `order_channel_id` - - - - - A string indicating the ID of the order channel to retrieve shipping rates from. - - - - - Yes - - - - - \- - - - - - - - `webhook_secret` - - - - - A string indicating the secret used to sign the HMAC in webhooks. - - - - - Yes - - - - - \- - - - - - - - `return_address` - - - - - An object that indicates the return address to use when fulfilling an order return. Refer to [Webshipper's API reference](https://docs.webshipper.io/#shipping_addresses) for accepted properties in this object. - - - - - Yes for returns. - - - - - \- - - - - - - - `coo_countries` - - - - - A string or an array of strings, each being an ISO 3 character country codes used when attaching a Certificate of Origin. To support all countries you can set the value to `all`. - - - - - No - - - - - `all` - - - - - - - `delete_on_cancel` - - - - - A boolean value that determines whether Webshipper orders should be deleted when its associated Medusa fulfillment is canceled. - - - - - No - - - - - `false` - - - - - - - `document_size` - - - - - A string indicating the size used when retrieving documents, such as fulfillment documents. The accepted values, are `100X150`, `100X192`, or `A4`. - - - - - No - - - - - `A4` - - - - - - - `return_portal` - - - - - An object that includes options related to order returns. It includes the following properties: - - - `id`: is a string indicating the ID of the return portal to use when fulfilling an order return. - - `cause_id`: is a string indicating the ID of the return cause to use when fulfilling an order return. - - `refund_method_id` is a string indicating the ID of the refund method to use when fulfilling an order return. - - - - - No - - - - - \- - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -WEBSHIPPER_ACCOUNT= -WEBSHIPPER_API_TOKEN= -WEBSHIPPER_ORDER_CHANNEL_ID= -WEBSHIPPER_WEBHOOK_SECRET= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, enable the Webshipper Fulfillment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers) or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionfulfillmentproviders). - -After enabling the provider, add shipping options for that provider using either the [Medusa Admin](!user-guide!/settings/regions/shipping-options) or the [Admin API Routes](https://docs.medusajs.com/api/admin#shipping-options_postshippingoptions). - -Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). - ---- - -## Personal Customs Numbers - -In countries like South Korea, a personal customs number is required to clear customs. The Webshipper plugin can pass this information to Webshipper given that the number is stored in `order.shipping_address.metadata.personal_customs_no`. - -### Add Field in Checkout Flow - -To allow the customer to pass their personal customs number along with the order, dynamically show an input field to the customer when they are shopping from a region that requires a personal customs number. Then, make sure that the `metadata` field includes the personal customs number when updating the cart's shipping address. - -```ts -const onUpdateAddress = async () => { - const address = { - first_name: "John", - last_name: "Johnson", - // ..., - metadata: { - // TODO the value should be replaced with the - // value entered by the customer - personal_customs_no: "my-customs-number", - }, - } - - await medusaClient.carts - .update(cartId, { - shipping_address: address, - }) - .then(() => { - console.log( - "Webshipper will pass along the customs number" - ) - }) -} -``` diff --git a/www/apps/resources/app/_plugins/notifications/mailchimp/page.mdx b/www/apps/resources/app/_plugins/notifications/mailchimp/page.mdx deleted file mode 100644 index cb5a4af78c..0000000000 --- a/www/apps/resources/app/_plugins/notifications/mailchimp/page.mdx +++ /dev/null @@ -1,238 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Mailchimp Plugin`, -} - -# {metadata.title} - -## Features - -[Mailchimp](https://mailchimp.com) is an email marketing service used to create newsletters and subscriptions. - -By integrating Mailchimp with Medusa, customers can subscribe from Medusa to your Mailchimp newsletter and are automatically added to your Mailchimp subscribers list. - ---- - -## Install the Mailchimp Plugin - - - -- [Mailchimp account](https://mailchimp.com/signup) -- [Mailchimp API Key](https://mailchimp.com/help/about-api-keys/#Find_or_generate_your_API_key) -- [Mailchimp Audience ID](https://mailchimp.com/help/find-audience-id/) - - - -To install the Mailchimp plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-mailchimp -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "api_key", "The Mailchimp API Key."], - ["7", "newsletter_list_id", "The Mailchimp Audience ID."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ..., - { - resolve: `medusa-plugin-mailchimp`, - options: { - api_key: process.env.MAILCHIMP_API_KEY, - newsletter_list_id: - process.env.MAILCHIMP_NEWSLETTER_LIST_ID, - }, - }, -] -``` - -### Mailchimp Plugin Options - - - - - Option - Description - - - - - - - `api_key` - - - - - A string indicating the Mailchimp API Key. - - - - - - - `newsletter_list_id` - - - - - A string indicating the Mailchimp Audience ID. - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -MAILCHIMP_API_KEY= -MAILCHIMP_NEWSLETTER_LIST_ID= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -This plugin adds new `POST` and `PUT` API Routes at `/mailchimp/subscribe`. These API Routes require in the body of the request an `email` field. You can also optionally include a `data` object that holds any additional data you want to send to Mailchimp. - -Check out [Mailchimp’s subscription documentation](https://mailchimp.com/developer/marketing/api/list-merges/) for more details on the data you can send. - -### Without Additional Data - -Try sending a `POST` or `PUT` request to `/mailchimp/subscribe`: - -```bash noReport apiTesting testApiUrl="http://localhost:9000/mailchimp/subscribe" testApiMethod="POST" testBodyParams={{ "email": "example@gmail.com" }} -curl -X POST http://localhost:9000/mailchimp/subscribe \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "email": "example@gmail.com" - }' -``` - -When the subscription is successful, a `200` response code is returned with `OK` message. - -When the same email address is used again in the `POST`, a `400` response is returned. If this can occur in your usecase, use the `PUT` API Route instead. - -Check your Mailchimp dashboard, you should find the email added to your Audience list. - -### With Additional Data - -For example, send in the `data` request body parameter a `tags` array: - -```bash noReport apiTesting testApiUrl="http://localhost:9000/mailchimp/subscribe" testApiMethod="POST" testBodyParams={{ "email": "example@gmail.com" }} -curl -X POST http://localhost:9000/mailchimp/subscribe \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "email": "example@gmail.com", - "data": { - "tags": ["customer"] - } - }' -``` - -All fields inside `data` is sent to Mailchimp along with the email. - ---- - -## Use MailchimpService - -Use the `MailchimpService` to subscribe users to the newsletter in other contexts. This service has a method `subscribeNewsletter` that subscribes a customer to the newsletter. - -For example: - -```ts title="src/subscribers/customer-created.ts" -import { - type SubscriberConfig, - type SubscriberArgs, - CustomerService, -} from "@medusajs/medusa" - -export default async function handleCustomerCreated({ - data, - container, -}: SubscriberArgs>) { - const mailchimpService = container.resolve("mailchimpService") - - mailchimpService.subscribeNewsletter( - data.email, - { tags: ["customer"] } // optional - ) -} - -export const config: SubscriberConfig = { - event: CustomerService.Events.CREATED, -} -``` - -This creates a subscriber that listens to the `CustomerService.Events.CREATED` (`customer.created`) event and subscribes the customer automatically using the `mailchimpService`. - ---- - -## Add Subscription Form - -This section provides a simple example of adding a subscription form in your storefront. The code is for React-based frameworks, but you can use the same logic for your storefronts regardless of the framework you are using. - -You need to use [axios](https://github.com/axios/axios) to send API requests, so start by installing it in your storefront project: - -```bash npm2yarn -npm install axios -``` - -Then, create the following component that uses the mailchimp plugin's API route to subscribe customers: - -```tsx -import axios from "axios" -import { useState } from "react" - -export default function NewsletterForm() { - const [email, setEmail] = useState("") - - function subscribe(e) { - e.preventDefault() - if (!email) { - return - } - - axios.post("http://localhost:9000/mailchimp/subscribe", { - email, - }) - .then((e) => { - alert("Subscribed successfully!") - setEmail("") - }) - .catch((e) => { - console.error(e) - alert("An error occurred") - }) - } - - return ( -
-

Sign Up for our newsletter

- setEmail(e.target.value)} - /> - -
- ) -} -``` diff --git a/www/apps/resources/app/_plugins/notifications/sendgrid/page.mdx b/www/apps/resources/app/_plugins/notifications/sendgrid/page.mdx deleted file mode 100644 index a2bb75acd6..0000000000 --- a/www/apps/resources/app/_plugins/notifications/sendgrid/page.mdx +++ /dev/null @@ -1,4570 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `SendGrid Plugin`, -} - -# {metadata.title} - -## Features - -[SendGrid](https://sendgrid.com) is an email and notification service used to send emails to your customers and users. - -By integrating SendGrid with Medusa, you’ll be sending email notifications to your users in the following cases: - -1. Order-related events include new orders, shipments created, and orders canceled. -2. Swaps and Returns related events including new return requests of orders and items returned successfully. -3. When Gift Cards in an order are created. -4. User-related events including reset passwords. -5. Restock Notifications for when product stocks are low. - -You can also handle custom events. - ---- - -## Install the SendGrid Plugin - - - -- [SendGrid account](https://signup.sendgrid.com) -- [Setup SendGrid single sender](https://docs.sendgrid.com/ui/sending-email/sender-verification) -- [SendGrid API Key](https://docs.sendgrid.com/ui/account-and-settings/api-keys) -- [SendGrid email templates](https://docs.sendgrid.com/ui/sending-email/how-to-send-an-email-with-dynamic-templates). Refer to the [Template Reference](#template-reference) for a full list of template types and data sent from Medusa. - - - -To install the SendGrid plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-sendgrid -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "api_key", "The SendGrid API Key."], - ["7", "from", "The from email."], - ["8", "order_placed_template", "The ID of the template to use when sending an order confirmation email to the customer."], - ["10", "localization", "Add different templates for different locales."], - ["12", "order_placed_template", "The template to use when sending an order confirmation email to a customer in Germany."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ..., - { - resolve: `medusa-plugin-sendgrid`, - options: { - api_key: process.env.SENDGRID_API_KEY, - from: process.env.SENDGRID_FROM, - order_placed_template: - process.env.SENDGRID_ORDER_PLACED_ID, - localization: { - "de-DE": { // locale key - order_placed_template: - process.env.SENDGRID_ORDER_PLACED_ID_LOCALIZED, - }, - }, - }, - }, -] -``` - -### SendGrid Plugin Options - - - - - Option - Description - Required - - - - - - - `api_key` - - - - - A string indicating the SendGrid API Key. - - - - - Yes - - - - - - - `from` - - - - - A string indicating the from email. - - - - - Yes - - - - - - - `order_placed_template` - - - - - A string indicating the ID of the template to use when sending an order confirmation email to the customer. It's triggered by the `order.placed` event. - - - - - No - - - - - - - `order_canceled_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when their order is canceled. It's triggered by the `order.canceled` event. - - - - - No - - - - - - - `order_shipped_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when their order is shipped. It's triggered by the `order.shipment_created` event. - - - - - No - - - - - - - `order_return_requested_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when the admin requests a return. It's triggered by the `order.return_requested` event. - - - - - No - - - - - - - `order_items_returned_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when an item is returned. It's triggered by the `order.items_returned` event. - - - - - No - - - - - - - `order_refund_created_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when their order is refunded. It's triggered by the `order.refund_created` event. - - - - - No - - - - - - - `claim_shipment_created_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when a shipment is created for their order claim. It's triggered by the `claim.shipment_created` event. - - - - - No - - - - - - - `swap_created_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when the admin creates a swap for their order. It's triggered by the `swap.created` event. - - - - - No - - - - - - - `swap_shipment_created_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when a shipment is created for their swap. It's triggered by the `swap.shipment_created` event. - - - - - No - - - - - - - `swap_received_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when their swap is received. It's triggered by the `swap.received` event. - - - - - No - - - - - - - `gift_card_created_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when a gift card is created for them or when they purchase a gift card. It's triggered by the `gift_card.created` and `order.gift_card_created` events. - - - - - No - - - - - - - `customer_password_reset_template` - - - - - A string indicating the ID of the template to use when sending an email to the customer when they request to reset their password. It's triggered by the `customer.password_reset` event. - - - - - No - - - - - - - `user_password_reset_template` - - - - - A string indicating the ID of the template to use when sending an email to the admin user when they request to reset their password. It's triggered by the `user.password_reset` event. - - - - - No - - - - - - - `medusa_restock_template` - - - - - A string indicating the ID of the template to use when sending an email to the admin user when a product has hit the restock quantity threshold. It's triggered by the `restock-notification.restocked` event. - - - - - No - - - - - - - `localization` - - - - - An object used to specify different template IDs for specific locales. Its keys are [ISO-8859-1 locales](https://en.wikipedia.org/wiki/ISO/IEC_8859-1) and values are objects. Each object's keys are template names similar to those you pass to the plugin's options, such as `order_placed_template`, and its value the ID of that template for the specified locale. - - For a localization template to be used, the locale key must be stored in the `cart.context.locale` of the cart associated with the event, such as the cart associated with the placed order. For other types of events that aren't associated with a cart, such as custom events, the locale must be in the event payload object under the `locale` key. - - - - - No - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -SENDGRID_API_KEY= -SENDGRID_FROM= -SENDGRID_ORDER_PLACED_ID= -``` - -### Custom Templates - -Aside from the supported templates mentioned in the [plugin options](#sendgrid-plugin-options) section, you may specify templates for Medusa defined or custom events. - -For example: - -```js title="medusa-config.js" -const plugins = [ - // ..., - { - resolve: `medusa-plugin-sendgrid`, - options: { - // other options... - product_created_template: - process.env.SENDGRID_ORDER_CREATED_ID, - my_custom_event_template: - process.env.SENDGRID_CUSTOM_EVENT_ID, - }, - }, -] -``` - -Make sure to replace every `.` in the event's name with a `_`. - -The data passed to the template is the event's data payload. - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, perform an action that triggers sending an email, such as requesting password reset or placing an order. An email from your SendGrid account will be sent to the customer email. - ---- - -## Use SendgridService - -Use the `SendgridService` to send emails in other contexts. The service has a `sendEmail` method that accepts an object of the email's details. - -For example, in an API Route: - -```ts title="src/api/store/email/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/medusa" - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const sendgridService = req.scope.resolve("sendgridService") - const sendOptions = { - templateId: "d-123....", - from: "ACME ", - to: "customer@mail.com", - dynamic_template_data: { dynamic: "data" }, - } - sendgridService.sendEmail(sendOptions) -} -``` - -The `sendEmail` method accepts the parameters mentioned in [SendGrid's API reference](https://docs.sendgrid.com/api-reference/mail-send/mail-send). - ---- - -## Template Reference - -This section covers the template types supported by the plugin and what variables to expect in your dynamic template. You can use the variables to add details like order total or customer name. - -### Order Placed - -**Key in plugin options:** `order_placed_template` - -**Triggering Event:** `order.placed` - -**Description:** Template to use when sending an order confirmation email to the customer. - -
- - ```json noReport - { - "afterLoad": [Function], - "beforeInsert": [Function], - "beforeUpdate": [Function], - "billing_address": null, - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "claims": Array [], - "created_at": Any, - "currency_code": "usd", - "customer": Object { - "billing_address_id": null, - "created_at": Any, - "deleted_at": null, - "email": "test@testson.com", - "first_name": "Reba", - "has_account": true, - "id": Any, - "last_name": "Waelchi", - "metadata": null, - "phone": "1-308-426-4696", - "updated_at": Any, - }, - "customer_id": Any, - "date": Any, - "discount_total": "0.00 USD", - "discounts": Array [], - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "fulfillments": Array [], - "gift_card_tax_total": 0, - "gift_card_total": "0.00 USD", - "gift_card_transactions": Array [], - "gift_cards": Array [], - "has_discounts": 0, - "has_gift_cards": 0, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "adjustments": Array [], - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "discount_total": 0, - "discounted_price": "12.00 USD", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": "test-item", - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "original_tax_total": 400, - "original_total": 2400, - "price": "12.00 USD", - "quantity": 2, - "raw_discount_total": 0, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "subtotal": 2000, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 400, - "thumbnail": null, - "title": "Incredible Plastic Mouse", - "total": 2400, - "totals": Object { - "discount_total": 0, - "original_tax_total": 400, - "original_total": 2400, - "quantity": 2, - "raw_discount_total": 0, - "subtotal": 2000, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 400, - "total": 2400, - "unit_price": 1000, - }, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "locale": null, - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "payments": Array [], - "refunded_total": 0, - "refunds": Array [], - "region": Object { - "automatic_taxes": true, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "gift_cards_taxable": true, - "id": "test-region", - "metadata": null, - "name": "Test region", - "tax_code": null, - "tax_provider_id": null, - "tax_rate": 12.5, - "updated_at": Any, - }, - "region_id": "test-region", - "returns": Array [], - "sales_channel_id": null, - "shipping_address": Object { - "address_1": "1850 Corine Tunnel", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Cornell", - "id": Any, - "last_name": "Shanahan", - "metadata": null, - "phone": null, - "postal_code": "15102-3998", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "shipping_methods": Array [], - "shipping_total": "0.00 USD", - "status": "pending", - "subtotal": "24.00 USD", - "subtotal_ex_tax": "20.00 USD", - "swaps": Array [], - "tax_rate": null, - "tax_total": "4.00 USD", - "total": "24.00 USD", - "updated_at": Any, - } - ``` - - - -### Order Canceled - -**Key in plugin options:** `order_canceled_template` - -**Triggering Event:** `order.canceled` - -**Description:** Template to use when sending an email to the customer when their order is canceled. - -
- - ```json noReport - { - "afterLoad": [Function], - "beforeInsert": [Function], - "beforeUpdate": [Function], - "billing_address": null, - "billing_address_id": null, - "canceled_at": Any, - "cart_id": null, - "claims": Array [], - "created_at": Any, - "currency_code": "usd", - "customer": Object { - "billing_address_id": null, - "created_at": Any, - "deleted_at": null, - "email": "test@testson.com", - "first_name": "Lonzo", - "has_account": true, - "id": Any, - "last_name": "Kemmer", - "metadata": null, - "phone": "499-811-0832", - "updated_at": Any, - }, - "customer_id": Any, - "date": Any, - "discount_total": "0.00 USD", - "discounts": Array [], - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "canceled", - "fulfillments": Array [], - "gift_card_tax_total": 0, - "gift_card_total": "0.00 USD", - "gift_card_transactions": Array [], - "gift_cards": Array [], - "has_discounts": 0, - "has_gift_cards": 0, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "adjustments": Array [], - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "discount_total": 0, - "fulfilled_quantity": null, - "has_shipping": null, - "id": "test-item", - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "original_tax_total": 400, - "original_total": 2400, - "price": "10.00 USD", - "quantity": 2, - "raw_discount_total": 0, - "returned_quantity": null, - "shipped_quantity": null, - "should_merge": true, - "subtotal": 2000, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 400, - "thumbnail": null, - "title": "Handcrafted Frozen Tuna", - "total": 2400, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "awesome-metal-ball", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "locale": null, - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "canceled", - "payments": Array [], - "refunded_total": 0, - "refunds": Array [], - "region": Object { - "automatic_taxes": true, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "gift_cards_taxable": true, - "id": "test-region", - "metadata": null, - "name": "Test region", - "tax_code": null, - "tax_provider_id": null, - "tax_rate": 12.5, - "updated_at": Any, - }, - "region_id": "test-region", - "returns": Array [], - "sales_channel_id": null, - "shipping_address": Object { - "address_1": "84185 Lindsey Centers", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Chyna", - "id": Any, - "last_name": "Osinski", - "metadata": null, - "phone": null, - "postal_code": "51510", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "shipping_methods": Array [ - Object { - "cart_id": null, - "claim_order_id": null, - "data": Object {}, - "id": Any, - "order_id": Any, - "price": 0, - "return_id": null, - "shipping_option": Object { - "admin_only": false, - "amount": 500, - "created_at": Any, - "data": Object {}, - "deleted_at": null, - "id": Any, - "is_return": false, - "metadata": null, - "name": "free", - "price_type": "flat_rate", - "profile_id": Any, - "provider_id": "test-ful", - "region_id": "test-region", - "updated_at": Any, - }, - "shipping_option_id": Any, - "swap_id": null, - "tax_lines": Array [], - }, - ], - "shipping_total": "0.00 USD", - "status": "canceled", - "subtotal": "20.00 USD", - "swaps": Array [], - "tax_rate": null, - "tax_total": "4.00 USD", - "total": "24.00 USD", - "updated_at": Any, - } - ``` - - - -### Order Shipment Created - -**Key in plugin options:** `order_shipped_template` - -**Triggering Event:** `order.shipment_created` - -**Description:** Template to use when sending an email to the customer when their order is shipped. - -
- - ```json noReport - { - "date": Any, - "email": "test@testson.com", - "fulfillment": Object { - "canceled_at": null, - "claim_order_id": null, - "created_at": Any, - "data": Object {}, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "fulfillment_id": Any, - "item_id": "test-item", - "quantity": 2, - }, - ], - "location_id": null, - "metadata": Object {}, - "no_notification": null, - "order_id": Any, - "provider_id": "test-ful", - "shipped_at": Any, - "swap_id": null, - "tracking_links": Array [], - "tracking_numbers": Array [], - "updated_at": Any, - }, - "locale": null, - "order": Object { - "afterLoad": [Function], - "beforeInsert": [Function], - "beforeUpdate": [Function], - "billing_address": null, - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "claims": Array [], - "created_at": Any, - "currency_code": "usd", - "customer": Object { - "billing_address_id": null, - "created_at": Any, - "deleted_at": null, - "email": "test@testson.com", - "first_name": "Reba", - "has_account": true, - "id": Any, - "last_name": "Waelchi", - "metadata": null, - "phone": "1-308-426-4696", - "updated_at": Any, - }, - "customer_id": Any, - "discount_total": 0, - "discounts": Array [], - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "shipped", - "fulfillments": Array [ - Object { - "canceled_at": null, - "claim_order_id": null, - "created_at": Any, - "data": Object {}, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "fulfillment_id": Any, - "item_id": "test-item", - "quantity": 2, - }, - ], - "location_id": null, - "metadata": Object {}, - "no_notification": null, - "order_id": Any, - "provider_id": "test-ful", - "shipped_at": Any, - "swap_id": null, - "tracking_numbers": Array [], - "updated_at": Any, - }, - ], - "gift_card_tax_total": 0, - "gift_card_total": 0, - "gift_card_transactions": Array [], - "gift_cards": Array [], - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "adjustments": Array [], - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "discount_total": 0, - "fulfilled_quantity": 2, - "has_shipping": null, - "id": "test-item", - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "original_tax_total": 400, - "original_total": 2400, - "quantity": 2, - "raw_discount_total": 0, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "subtotal": 2000, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 400, - "thumbnail": "", - "title": "Incredible Plastic Mouse", - "total": 2400, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "payments": Array [], - "refundable_amount": 0, - "refunded_total": 0, - "refunds": Array [], - "region": Object { - "automatic_taxes": true, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "gift_cards_taxable": true, - "id": "test-region", - "metadata": null, - "name": "Test region", - "tax_code": null, - "tax_provider_id": null, - "tax_rate": 12.5, - "updated_at": Any, - }, - "region_id": "test-region", - "returns": Array [], - "sales_channel_id": null, - "shipping_address": Object { - "address_1": "1850 Corine Tunnel", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Cornell", - "id": Any, - "last_name": "Shanahan", - "metadata": null, - "phone": null, - "postal_code": "15102-3998", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "shipping_methods": Array [ - Object { - "cart_id": null, - "claim_order_id": null, - "data": Object {}, - "id": Any, - "order_id": Any, - "price": 0, - "return_id": null, - "shipping_option": Object { - "admin_only": false, - "amount": 500, - "created_at": Any, - "data": Object {}, - "deleted_at": null, - "id": Any, - "is_return": false, - "metadata": null, - "name": "free", - "price_type": "flat_rate", - "profile_id": Any, - "provider_id": "test-ful", - "region_id": "test-region", - "updated_at": Any, - }, - "shipping_option_id": Any, - "swap_id": null, - "tax_lines": Array [], - }, - ], - "shipping_total": 0, - "status": "pending", - "subtotal": 2000, - "swaps": Array [], - "tax_rate": null, - "tax_total": 400, - "total": 2400, - "updated_at": Any, - }, - "tracking_links": Array [], - "tracking_number": "", - } - ``` - - - -### Order Return Requested - -**Key in plugin options:** `order_return_requested_template` - -**Triggering Event:** `order.return_requested` - -**Description:** Template to use when sending an email to the customer when the admin requests a return. - -
- - ```json noReport - { - "date": Any, - "email": "test@testson.com", - "has_shipping": false, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "price": "12.00 USD", - "quantity": 1, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Incredible Plastic Mouse", - "totals": Object { - "discount_total": 0, - "original_tax_total": 200, - "original_total": 1200, - "quantity": 1, - "raw_discount_total": 0, - "subtotal": 1000, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 200, - "total": 1200, - "unit_price": 1000, - }, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "locale": null, - "order": Object { - "afterLoad": [Function], - "beforeInsert": [Function], - "beforeUpdate": [Function], - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "claims": Array [], - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "discounts": Array [], - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "gift_card_transactions": Array [], - "gift_cards": Array [], - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "adjustments": Array [], - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "discount_total": 0, - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "original_tax_total": 400, - "original_total": 2400, - "quantity": 2, - "raw_discount_total": 0, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "subtotal": 2000, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 400, - "thumbnail": "", - "title": "Incredible Plastic Mouse", - "total": 2400, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "refunds": Array [], - "region": Object { - "automatic_taxes": true, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "gift_cards_taxable": true, - "id": Any, - "metadata": null, - "name": "Test region", - "tax_code": null, - "tax_provider_id": null, - "tax_rate": 12.5, - "updated_at": Any, - }, - "region_id": "test-region", - "returns": Array [ - Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": Any, - "items": Array [ - Object { - "is_requested": true, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": null, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "location_id": null, - "metadata": null, - "no_notification": null, - "order_id": Any, - "received_at": null, - "refund_amount": 1200, - "shipping_data": null, - "status": "requested", - "swap_id": null, - "updated_at": Any, - }, - ], - "sales_channel_id": null, - "shipping_address": Object { - "address_1": "1850 Corine Tunnel", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Cornell", - "id": Any, - "last_name": "Shanahan", - "metadata": null, - "phone": null, - "postal_code": "15102-3998", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "shipping_methods": Array [], - "status": "pending", - "swaps": Array [], - "tax_rate": null, - "total": 2400, - "updated_at": Any, - }, - "refund_amount": "12.00 USD", - "return_request": Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": Any, - "items": Array [ - Object { - "is_requested": true, - "item": Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": "", - "title": "Incredible Plastic Mouse", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": null, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "location_id": null, - "metadata": null, - "no_notification": null, - "order_id": Any, - "received_at": null, - "refund_amount": "12.00 USD", - "shipping_data": null, - "shipping_method": null, - "status": "requested", - "swap_id": null, - "updated_at": Any, - }, - "shipping_total": "0.00 USD", - "subtotal": "12.00 USD", - } - ``` - - - -### Order Items Returned - -**Key in plugin options:** `order_items_returned_template` - -**Triggering Event:** `order.items_returned` - -**Description:** Template to use when sending an email to the customer when an item is returned. - -
- - ```json noReport - { - "date": Any, - "email": "test@testson.com", - "has_shipping": false, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "price": "12.00 USD", - "quantity": 1, - "returned_quantity": 1, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Incredible Plastic Mouse", - "totals": Object { - "discount_total": 0, - "original_tax_total": 200, - "original_total": 1200, - "quantity": 1, - "raw_discount_total": 0, - "subtotal": 1000, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 200, - "total": 1200, - "unit_price": 1000, - }, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "locale": null, - "order": Object { - "afterLoad": [Function], - "beforeInsert": [Function], - "beforeUpdate": [Function], - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "claims": Array [], - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "discounts": Array [], - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "requires_action", - "gift_card_transactions": Array [], - "gift_cards": Array [], - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "adjustments": Array [], - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "discount_total": 0, - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "original_tax_total": 400, - "original_total": 2400, - "quantity": 2, - "raw_discount_total": 0, - "returned_quantity": 1, - "shipped_quantity": 2, - "should_merge": true, - "subtotal": 2000, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 400, - "thumbnail": "", - "title": "Incredible Plastic Mouse", - "total": 2400, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "refunds": Array [], - "region": Object { - "automatic_taxes": true, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "gift_cards_taxable": true, - "id": Any, - "metadata": null, - "name": "Test region", - "tax_code": null, - "tax_provider_id": null, - "tax_rate": 12.5, - "updated_at": Any, - }, - "region_id": "test-region", - "returns": Array [ - Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": Any, - "items": Array [ - Object { - "is_requested": true, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": 1, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "location_id": null, - "metadata": null, - "no_notification": null, - "order_id": Any, - "received_at": Any, - "refund_amount": 1200, - "shipping_data": null, - "status": "received", - "swap_id": null, - "updated_at": Any, - }, - ], - "sales_channel_id": null, - "shipping_address": Object { - "address_1": "1850 Corine Tunnel", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Cornell", - "id": Any, - "last_name": "Shanahan", - "metadata": null, - "phone": null, - "postal_code": "15102-3998", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "shipping_methods": Array [], - "status": "pending", - "swaps": Array [], - "tax_rate": null, - "total": 2400, - "updated_at": Any, - }, - "refund_amount": "12.00 USD", - "return_request": Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": Any, - "items": Array [ - Object { - "is_requested": true, - "item": Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "quantity": 2, - "returned_quantity": 1, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": "", - "title": "Incredible Plastic Mouse", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": 1, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "location_id": null, - "metadata": null, - "no_notification": null, - "order_id": Any, - "received_at": Any, - "refund_amount": "12.00 USD", - "shipping_data": null, - "shipping_method": null, - "status": "received", - "swap_id": null, - "updated_at": Any, - }, - "shipping_total": "0.00 USD", - "subtotal": "12.00 USD", - } - ``` - - - -### Order Refund Created - -**Key in plugin options:** `order_refund_created_template` - -**Triggering Event:** `order.refund_created` - -**Description:** Template to use when sending an email to the customer when their order is refunded. - -
- - ```json noReport - { - "order": Object { - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "thumbnail": "", - "title": "Incredible Plastic Mouse", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "region_id": "test-region", - "sales_channel_id": null, - "shipping_address": Object { - "address_1": "1850 Corine Tunnel", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Cornell", - "id": Any, - "last_name": "Shanahan", - "metadata": null, - "phone": null, - "postal_code": "15102-3998", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "status": "pending", - "tax_rate": null, - "updated_at": Any, - }, - "refund": { - "id": Any, - "order_id": Any, - "payment_id": Any, - "amount": Any, - "note": Any, - "reason": Any, - "idempotency_key": Any, - "created_at": Any, - "updated_at": Any, - "metadata": null - }, - "refund_amount": 1200, - "email": "test@testson.com" - } - ``` - - - -### Claim Shipment Created - -**Key in plugin options:** `claim_shipment_created_template` - -**Triggering Event:** `claim.shipment_created` - -**Description:** Template to use when sending an email to the customer when a shipment is created for their order claim. - -
- - ```json noReport - { - "claim": Object { - "canceled_at": null, - "created_at": Any, - "deleted_at": null, - "fulfillment_status": "shipped", - "id": Any, - "idempotency_key": Any, - "metadata": null, - "no_notification": null, - "order": Object { - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "thumbnail": "", - "title": "Incredible Plastic Mouse", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "region_id": "test-region", - "sales_channel_id": null, - "shipping_address": Object { - "address_1": "1850 Corine Tunnel", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Cornell", - "id": Any, - "last_name": "Shanahan", - "metadata": null, - "phone": null, - "postal_code": "15102-3998", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "status": "pending", - "tax_rate": null, - "updated_at": Any, - }, - "order_id": Any, - "payment_status": "na", - "refund_amount": null, - "shipping_address_id": Any, - "type": "replace", - "updated_at": Any, - }, - "email": "test@testson.com", - "fulfillment": Object { - "canceled_at": null, - "claim_order_id": Any, - "created_at": Any, - "data": Object {}, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "fulfillment_id": Any, - "item_id": Any, - "quantity": 1, - }, - ], - "location_id": null, - "metadata": Object {}, - "no_notification": null, - "order_id": null, - "provider_id": "test-ful", - "shipped_at": Any, - "swap_id": null, - "tracking_links": Array [], - "tracking_numbers": Array [], - "updated_at": Any, - }, - "locale": null, - "order": Object { - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "thumbnail": "", - "title": "Incredible Plastic Mouse", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "gorgeous-cotton-table", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Gorgeous Cotton Table", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Refined Wooden Chair", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "region_id": "test-region", - "sales_channel_id": null, - "shipping_address": Object { - "address_1": "1850 Corine Tunnel", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Cornell", - "id": Any, - "last_name": "Shanahan", - "metadata": null, - "phone": null, - "postal_code": "15102-3998", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "status": "pending", - "tax_rate": null, - "updated_at": Any, - }, - "tracking_links": Array [], - "tracking_number": "", - } - ``` - - - -### Swap Created - -**Key in plugin options:** `swap_created_template` - -**Triggering Event:** `swap.created` - -**Description:** Template to use when sending an email to the customer when the admin creates a swap for their order. - -
- - ```json noReport - { - "locale": null, - "swap": Object { - "additional_items": Array [ - Object { - "allow_discounts": true, - "cart_id": Any, - "claim_order_id": null, - "created_at": Any, - "description": "Small Wooden Computer", - "fulfilled_quantity": 1, - "has_shipping": true, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": Object {}, - "order_id": null, - "quantity": 1, - "returned_quantity": null, - "shipped_quantity": 1, - "should_merge": true, - "swap_id": Any, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": Any, - "metadata": null, - "name": "default", - "rate": 12.5, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Awesome Metal Ball", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "variant-2", - "inventory_quantity": 9, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Small Wooden Computer", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "variant-2", - }, - ], - "return_order": Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "is_requested": true, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": null, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "metadata": null, - "no_notification": null, - "order_id": null, - "received_at": null, - "refund_amount": 1200, - "shipping_data": null, - "status": "requested", - "swap_id": Any, - "updated_at": Any, - }, - "allow_backorder": true, - "canceled_at": null, - "cart_id": Any, - "confirmed_at": Any, - "created_at": Any, - "deleted_at": null, - "difference_due": 488, - "fulfillment_status": "shipped", - "id": Any, - "idempotency_key": Any, - "metadata": null, - "no_notification": null, - "order_id": Any, - "payment_status": "awaiting", - "shipping_address_id": Any, - "updated_at": Any, - }, - "order": Object { - "discounts": Array [], - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_id": Any, - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "thumbnail": "", - "title": "Intelligent Plastic Chips", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "region_id": "test-region", - "shipping_address": Object { - "address_1": "84185 Lindsey Centers", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Chyna", - "id": Any, - "last_name": "Osinski", - "metadata": null, - "phone": null, - "postal_code": "51510", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "status": "pending", - "tax_rate": null, - "updated_at": Any, - "swaps": Array [], - }, - "return_request": Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": Any, - "items": Array [ - Object { - "is_requested": true, - "item": Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_id": Any, - "quantity": 2, - "returned_quantity": 1, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": "", - "title": "Intelligent Plastic Chips", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 11, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": null, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "metadata": null, - "no_notification": null, - "order_id": Any, - "received_at": Any, - "refund_amount": "12.00 USD", - "shipping_data": null, - "shipping_method": null, - "status": "received", - "swap_id": null, - "updated_at": Any, - }, - "date": Any, - "swapLink": Any, - "email": "test@testson.com", - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "discounted_price": "12.00 USD", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": "test-item", - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_id": Any, - "price": "12.00 USD", - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Intelligent Plastic Chips", - "totals": Object { - "discount_total": 0, - "gift_card_total": 0, - "original_tax_total": 400, - "original_total": 2400, - "quantity": 2, - "subtotal": 2000, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 400, - "total": 2400, - "unit_price": 1000, - }, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "return_items": Array[...], //same as items - "return_total": "12.00 USD", - "refund_amount": "12.00 USD", - "additional_total": "11.25 USD" - } - ``` - -
- -### Swap Shipment Created - -**Key in plugin options:** `swap_shipment_created_template` - -**Triggering Event:** `swap.shipment_created` - -**Description:** Template to use when sending an email to the customer when a shipment is created for their swap. - -
- - ```json noReport - { - "additional_total": "16.88 USD", - "date": Any, - "email": "test@testson.com", - "fulfillment": Object { - "canceled_at": null, - "claim_order_id": null, - "created_at": Any, - "data": Object {}, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "fulfillment_id": Any, - "item_id": Any, - "quantity": 1, - }, - ], - "location_id": null, - "metadata": Object {}, - "no_notification": null, - "order_id": null, - "provider_id": "test-ful", - "shipped_at": Any, - "swap_id": Any, - "tracking_links": Array [], - "tracking_numbers": Array [], - "updated_at": Any, - }, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": Any, - "claim_order_id": null, - "created_at": Any, - "description": "Small Wooden Computer", - "discounted_price": "11.25 USD", - "fulfilled_quantity": 1, - "has_shipping": true, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": Object {}, - "order_edit_id": null, - "order_id": null, - "original_item_id": null, - "price": "11.25 USD", - "quantity": 1, - "returned_quantity": null, - "shipped_quantity": 1, - "should_merge": true, - "swap_id": Any, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": Any, - "metadata": null, - "name": "default", - "rate": 12.5, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Awesome Metal Ball", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "variant-2", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "awesome-metal-ball", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Small Wooden Computer", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "variant-2", - }, - ], - "locale": null, - "order": Object { - "afterLoad": [Function], - "beforeInsert": [Function], - "beforeUpdate": [Function], - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "discounts": Array [], - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_edit_id": null, - "order_id": Any, - "original_item_id": null, - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": Any, - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": "", - "title": "Handcrafted Frozen Tuna", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "awesome-metal-ball", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "region": Object { - "automatic_taxes": true, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "gift_cards_taxable": true, - "id": Any, - "metadata": null, - "name": "Test region", - "tax_code": null, - "tax_provider_id": null, - "tax_rate": 12.5, - "updated_at": Any, - }, - "region_id": "test-region", - "sales_channel_id": null, - "shipping_address_id": Any, - "status": "pending", - "swaps": Array [ - Object { - "additional_items": Array [ - Object { - "allow_discounts": true, - "cart_id": Any, - "claim_order_id": null, - "created_at": Any, - "description": "Small Wooden Computer", - "fulfilled_quantity": 1, - "has_shipping": true, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": Object {}, - "order_edit_id": null, - "order_id": null, - "original_item_id": null, - "quantity": 1, - "returned_quantity": null, - "shipped_quantity": 1, - "should_merge": true, - "swap_id": Any, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": Any, - "metadata": null, - "name": "default", - "rate": 12.5, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Awesome Metal Ball", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "variant-2", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "awesome-metal-ball", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Small Wooden Computer", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "variant-2", - }, - ], - "allow_backorder": true, - "canceled_at": null, - "cart_id": Any, - "confirmed_at": Any, - "created_at": Any, - "deleted_at": null, - "difference_due": 488, - "fulfillment_status": "shipped", - "id": Any, - "idempotency_key": Any, - "metadata": null, - "no_notification": null, - "order_id": Any, - "payment_status": "awaiting", - "shipping_address_id": Any, - "updated_at": Any, - }, - ], - "tax_rate": null, - "updated_at": Any, - }, - "paid_total": "4.88 USD", - "refund_amount": "12.00 USD", - "return_total": "12.00 USD", - "swap": Object { - "additional_items": Array [ - Object { - "allow_discounts": true, - "cart_id": Any, - "claim_order_id": null, - "created_at": Any, - "description": "Small Wooden Computer", - "fulfilled_quantity": 1, - "has_shipping": true, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": Object {}, - "order_edit_id": null, - "order_id": null, - "original_item_id": null, - "quantity": 1, - "returned_quantity": null, - "shipped_quantity": 1, - "should_merge": true, - "swap_id": Any, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": Any, - "metadata": null, - "name": "default", - "rate": 12.5, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Awesome Metal Ball", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "variant-2", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": "awesome-metal-ball", - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile": Any, - "profile_id": Any, - "profiles": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Small Wooden Computer", - "upc": null, - "updated_at": Any, - "variant_rank": 0, - "weight": null, - "width": null, - }, - "variant_id": "variant-2", - }, - ], - "allow_backorder": true, - "canceled_at": null, - "cart_id": Any, - "confirmed_at": Any, - "created_at": Any, - "deleted_at": null, - "difference_due": 488, - "fulfillment_status": "shipped", - "id": Any, - "idempotency_key": Any, - "metadata": null, - "no_notification": null, - "order_id": Any, - "payment_status": "awaiting", - "return_order": Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "is_requested": true, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": null, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "location_id": null, - "metadata": null, - "no_notification": null, - "order_id": null, - "received_at": null, - "refund_amount": 1200, - "shipping_data": null, - "status": "requested", - "swap_id": Any, - "updated_at": Any, - }, - "shipping_address": Object { - "address_1": "121 W Something St", - "address_2": null, - "city": "ville la something", - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Chyna", - "id": Any, - "last_name": "Osinski", - "metadata": null, - "phone": "12353245", - "postal_code": "1234", - "province": "something", - "updated_at": Any, - }, - "shipping_address_id": Any, - "shipping_methods": Array [ - Object { - "cart_id": Any, - "claim_order_id": null, - "data": Object {}, - "id": Any, - "order_id": null, - "price": 500, - "return_id": null, - "shipping_option": Object { - "admin_only": false, - "amount": 500, - "created_at": Any, - "data": Object {}, - "deleted_at": null, - "id": Any, - "is_return": false, - "metadata": null, - "name": "Test Method", - "price_type": "flat_rate", - "profile_id": Any, - "provider_id": "test-ful", - "region_id": "test-region", - "updated_at": Any, - }, - "shipping_option_id": Any, - "swap_id": Any, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "metadata": null, - "name": "default", - "rate": 12.5, - "shipping_method_id": Any, - "updated_at": Any, - }, - ], - }, - ], - "updated_at": Any, - }, - "tax_amount": "-0.12 USD", - "tracking_links": Array [], - "tracking_number": "", - } - ``` - - - -### Swap Received - -**Key in plugin options:** `swap_received_template` - -**Triggering Event:** `swap.received` - -**Description:** Template to use when sending an email to the customer when their swap is received. - -
- - ```json noReport - { - "locale": null, - "swap": Object { - "additional_items": Array [ - Object { - "allow_discounts": true, - "cart_id": Any, - "claim_order_id": null, - "created_at": Any, - "description": "Small Wooden Computer", - "fulfilled_quantity": 1, - "has_shipping": true, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": Object {}, - "order_id": null, - "quantity": 1, - "returned_quantity": null, - "shipped_quantity": 1, - "should_merge": true, - "swap_id": Any, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": Any, - "metadata": null, - "name": "default", - "rate": 12.5, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Awesome Metal Ball", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "variant-2", - "inventory_quantity": 9, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Small Wooden Computer", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "variant-2", - }, - ], - "return_order": Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "is_requested": true, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": null, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "metadata": null, - "no_notification": null, - "order_id": null, - "received_at": null, - "refund_amount": 1200, - "shipping_data": null, - "status": "requested", - "swap_id": Any, - "updated_at": Any, - }, - "allow_backorder": true, - "canceled_at": null, - "cart_id": Any, - "confirmed_at": Any, - "created_at": Any, - "deleted_at": null, - "difference_due": 488, - "fulfillment_status": "shipped", - "id": Any, - "idempotency_key": Any, - "metadata": null, - "no_notification": null, - "order_id": Any, - "payment_status": "awaiting", - "shipping_address_id": Any, - "updated_at": Any, - }, - "order": Object { - "discounts": Array [], - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_id": Any, - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "thumbnail": "", - "title": "Intelligent Plastic Chips", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "region_id": "test-region", - "shipping_address": Object { - "address_1": "84185 Lindsey Centers", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Chyna", - "id": Any, - "last_name": "Osinski", - "metadata": null, - "phone": null, - "postal_code": "51510", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "status": "pending", - "tax_rate": null, - "updated_at": Any, - "swaps": Array [], - }, - "return_request": Object { - "claim_order_id": null, - "created_at": Any, - "id": Any, - "idempotency_key": Any, - "items": Array [ - Object { - "is_requested": true, - "item": Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_id": Any, - "quantity": 2, - "returned_quantity": 1, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": "", - "title": "Intelligent Plastic Chips", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 11, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - "item_id": "test-item", - "metadata": null, - "note": null, - "quantity": 1, - "reason_id": null, - "received_quantity": null, - "requested_quantity": 1, - "return_id": Any, - }, - ], - "metadata": null, - "no_notification": null, - "order_id": Any, - "received_at": Any, - "refund_amount": "12.00 USD", - "shipping_data": null, - "shipping_method": null, - "status": "received", - "swap_id": null, - "updated_at": Any, - }, - "date": Any, - "swapLink": Any, - "email": "test@testson.com", - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "discounted_price": "12.00 USD", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": "test-item", - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_id": Any, - "price": "12.00 USD", - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "thumbnail": null, - "title": "Intelligent Plastic Chips", - "totals": Object { - "discount_total": 0, - "gift_card_total": 0, - "original_tax_total": 400, - "original_total": 2400, - "quantity": 2, - "subtotal": 2000, - "tax_lines": Array [ - Object { - "code": "default", - "created_at": Any, - "id": Any, - "item_id": "test-item", - "metadata": null, - "name": "default", - "rate": 20, - "updated_at": Any, - }, - ], - "tax_total": 400, - "total": 2400, - "unit_price": 1000, - }, - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "return_items": Array[...], //same as items - "return_total": "12.00 USD", - "tax_total": "4.00 USD", - "refund_amount": "12.00 USD", - "additional_total": "11.25 USD" - } - ``` - -
- -### Gift Card Created - -**Key in plugin options:** `gift_card_created_template` - -**Triggering Events:** `gift_card.created` and `order.gift_card_created` - -**Description:** Template to be to the customer sent when a gift card in their order has been created. - -
- - ```json noReport - { - "code": Any, - "value": 100, - "balance": 100, - "display_value": "100.00", - "region": Object { - "automatic_taxes": true, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "fulfillment_providers": Array [], - "gift_cards_taxable": true, - "id": Any, - "metadata": null, - "name": "Test region", - "payment_providers": Array [ - PaymentProvider { - "id": "test-pay", - "is_installed": true, - }, - ], - "tax_code": null, - "tax_provider_id": null, - "tax_rate": 12.5, - "updated_at": Any, - }, - "region_id": "test-region", - "order": Object { - "billing_address_id": null, - "canceled_at": null, - "cart_id": null, - "created_at": Any, - "currency_code": "usd", - "customer_id": Any, - "display_id": Any, - "draft_order_id": null, - "email": "test@testson.com", - "external_id": null, - "fulfillment_status": "fulfilled", - "id": Any, - "idempotency_key": null, - "items": Array [ - Object { - "allow_discounts": true, - "cart_id": null, - "claim_order_id": null, - "created_at": Any, - "description": "", - "fulfilled_quantity": 2, - "has_shipping": null, - "id": Any, - "is_giftcard": false, - "is_return": false, - "metadata": null, - "order_id": Any, - "quantity": 2, - "returned_quantity": null, - "shipped_quantity": 2, - "should_merge": true, - "swap_id": null, - "thumbnail": "", - "title": "Intelligent Plastic Chips", - "unit_price": 1000, - "updated_at": Any, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - }, - ], - "metadata": null, - "no_notification": null, - "object": "order", - "payment_status": "captured", - "region_id": "test-region", - "shipping_address": Object { - "address_1": "84185 Lindsey Centers", - "address_2": null, - "city": null, - "company": null, - "country_code": "us", - "created_at": Any, - "customer_id": null, - "deleted_at": null, - "first_name": "Chyna", - "id": Any, - "last_name": "Osinski", - "metadata": null, - "phone": null, - "postal_code": "51510", - "province": null, - "updated_at": Any, - }, - "shipping_address_id": Any, - "status": "pending", - "tax_rate": null, - "updated_at": Any, - }, - "order_id": Any, - "is_disabled": false, - "created_at": Any, - "updated_at": Any, - "deleted_at": null, - "locale": null, - "email": "test@testson.com", - "display_value": 4 - } - ``` - -
- -### Customer Password Reset - -**Key in plugin options:** `customer_password_reset_template` - -**Triggering Event:** `customer.password_reset` - -**Description:** Template to use when sending an email to the customer when they request to reset their password. - -
- - ```json noReport - { - "id": Any, - "email": "test@testson.com", - "first_name": "Chyna", - "last_name": "Osinski", - "token": Any, - } - ``` - -
- -### User Password Reset - -**Key in plugin options:** `user_password_reset_template` - -**Triggering Event:** `user.password_reset` - -**Description:** Template to use when sending an email to the admin user when they request to reset their password. - -
- - ```json noReport - { - "email": "test@testson.com", - "token": Any, - } - ``` - -
- -### Restock Notification - -**Key in plugin options:** medusa_restock_template - -**Triggering Event:** `restock-notification.restocked` - -**Description:** Template to use when sending an email to the admin user when a product has hit the restock quantity threshold. - -
- - ```json noReport - { - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": "", - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant": Object { - "allow_backorder": false, - "barcode": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "product": Object { - "collection_id": null, - "created_at": Any, - "deleted_at": null, - "description": null, - "discountable": true, - "external_id": null, - "handle": null, - "height": null, - "hs_code": null, - "id": "test-product", - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "origin_country": null, - "profile_id": Any, - "status": "draft", - "subtitle": null, - "thumbnail": null, - "title": "Awesome Metal Ball", - "type_id": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "product_id": "test-product", - "sku": null, - "title": "Practical Granite Pizza", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - "variant_id": "test-variant", - "emails": Array [ - "test@testson.com" - ] - } - ``` - -
- diff --git a/www/apps/resources/app/_plugins/notifications/slack/page.mdx b/www/apps/resources/app/_plugins/notifications/slack/page.mdx deleted file mode 100644 index d0426b44fa..0000000000 --- a/www/apps/resources/app/_plugins/notifications/slack/page.mdx +++ /dev/null @@ -1,160 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Slack Plugin`, -} - -# {metadata.title} - -## Features - -Slack is a communication platform used by teams and organizations for collaboration and messaging. This plugin sends merchants a slack message when a new order is placed. - -The notification contains details about the order including: - -- Customer's details and address. -- Items ordered, their quantity, and the price. -- Order totals including Tax amount. -- Promotion details if there are any (this is optional and can be turned off). - ---- - -## Install the Slack Plugin - - - -- [Slack account](https://slack.com) -- [A Slack app](https://api.slack.com/start/quickstart#creating) -- [Activate incoming webhooks in Slack and create a new webhook](https://api.slack.com/start/quickstart#webhooks) - - - -To install the Slack plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-slack-notification -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "slack_url", "The Slack webhook URL."], - ["7", "show_discount_code", "Whether to show the discount code after creating the Slack app."], - ["8", "admin_orders_url", "The prefix of the URL of the order detail pages on your admin panel."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-plugin-slack-notification`, - options: { - slack_url: process.env.SLACK_WEBHOOK_URL, - show_discount_code: false, - admin_orders_url: `http://localhost:7001/a/orders`, - }, - }, -] -``` - -### Twilio SMS Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `slack_url` - - - - - A string indicating the Slack webhook URL. - - - - - Yes - - - - - \- - - - - - - - `show_discount_code` - - - - - A boolean whether to show the discount code after creating the Slack app. - - - - - No - - - - - `false` - - - - - - - `admin_orders_url` - - - - - A string indicating the prefix of the URL of the order detail pages on your admin panel. - If you’re using Medusa Admin locally, it should be `http://localhost:7001/a/orders`. This results in a URL like `http://localhost:7001/a/orders/order_01FYP7DM7PS43H9VQ1PK59ZR5G`. - - - - - No, but if not provided the order URL in the messages will be `/order_01FYP7DM7PS43H9VQ1PK59ZR5G` - - - - - \- - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -SLACK_WEBHOOK_URL= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). A message is sent to the DM or slack channel you configured in the Slack webhook. diff --git a/www/apps/resources/app/_plugins/notifications/twilio-sms/page.mdx b/www/apps/resources/app/_plugins/notifications/twilio-sms/page.mdx deleted file mode 100644 index 51cae56b6d..0000000000 --- a/www/apps/resources/app/_plugins/notifications/twilio-sms/page.mdx +++ /dev/null @@ -1,165 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Twilio SMS Plugin`, -} - -# {metadata.title} - -## Features - -[Twilio’s SMS API](https://www.twilio.com/sms) is used to send users SMS messages instantly. It has a lot of additional features such as Whatsapp messaging and conversations. - -By integrating Twilio SMS into Medusa, you’ll have easy access to Twilio’s SMS API to send SMS messages to your users and customers. You can use it to send order confirmations, verification codes, reset password messages, and more. - -This plugin only gives you access to the Twilio SMS API but doesn't automate sending messages. You’ll have to add this yourself where you need it. There's an [example later in this guide](#example-plugin-usage) on how to send an SMS for a new order. - ---- - -## Install the Twilio SMS Plugin - - - -- [Twilio account](https://www.twilio.com/sms) -- [Twilio account SID](https://help.twilio.com/articles/14726256820123-What-is-a-Twilio-Account-SID-and-where-can-I-find-it-) -- [Twilio auth token](https://help.twilio.com/articles/223136027-Auth-Tokens-and-How-to-Change-Them?_gl=1*qv22ht*_ga*OTY3NzYwMDAzLjE2OTE0MjA5MDI.*_ga_RRP8K4M4F3*MTcwOTIwMDA2Ny40LjAuMTcwOTIwMDA2Ny4wLjAuMA..) -- [Twilio phone number](https://help.twilio.com/articles/223135247) - - - -To install the Twilio SMS plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-twilio-sms -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "account_sid", "The Twilio account SID."], - ["7", "auth_token", "The Twilio auth token."], - ["8", "from_number", "The Twilio phone number."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-plugin-twilio-sms`, - options: { - account_sid: process.env.TWILIO_SMS_ACCOUNT_SID, - auth_token: process.env.TWILIO_SMS_AUTH_TOKEN, - from_number: process.env.TWILIO_SMS_FROM_NUMBER, - }, - }, -] -``` - -### Twilio SMS Plugin Options - - - - - Option - Description - - - - - - - `account_sid` - - - - - A string indicating the Twilio account SID. - - - - - - - `auth_token` - - - - - A string indicating the Twilio auth token. - - - - - - - `from_number` - - - - - A string indicating the Twilio phone number. - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -TWILIO_SMS_ACCOUNT_SID= -TWILIO_SMS_AUTH_TOKEN= -TWILIO_SMS_FROM_NUMBER= -``` - ---- - -## Example Plugin Usage - -Resolve and use the `TwilioSmsService` to send SMS. - -For example, create the file `src/subscriber/sms.ts` with the following content: - -```ts title="src/subscriber/sms.ts" -import { - type SubscriberConfig, - type SubscriberArgs, - OrderService, -} from "@medusajs/medusa" - -export default async function handleOrderPlaced({ - data, - container, -}: SubscriberArgs>) { - const twilioSmsService = container.resolve("twilioSmsService") - const orderService: OrderService = - container.resolve("orderService") - - const order = await orderService.retrieve(data.id, { - relations: ["shipping_address"], - }) - - if (order.shipping_address.phone) { - twilioSmsService.sendSms({ - to: order.shipping_address.phone, - body: "We have received your order #" + data.id, - }) - } -} - -export const config: SubscriberConfig = { - event: OrderService.Events.PLACED, -} -``` - -This creates a subscriber that listens to the `OrderService.Events.PLACED` (`order.placed`) event and sends an SMS to the customer confirming their order. - -The `sendSms` method of the `TwilioSmsService` accepts an object whose shape is as described in [Twilio's API reference](https://www.twilio.com/docs/sms/api/message-resource#create-a-message-resource). - - - -If you’re on a Twilio trial make sure that the phone number you entered on checkout is a [verified Twilio number on your console](https://console.twilio.com/us1/develop/phone-numbers/manage/verified). - - diff --git a/www/apps/resources/app/_plugins/other/discount-generator/page.mdx b/www/apps/resources/app/_plugins/other/discount-generator/page.mdx deleted file mode 100644 index 47372fced5..0000000000 --- a/www/apps/resources/app/_plugins/other/discount-generator/page.mdx +++ /dev/null @@ -1,96 +0,0 @@ -export const metadata = { - title: `Discount Generator Plugin`, -} - -# {metadata.title} - -## Features - -In Medusa, merchants can create dynamic discounts that act as a template for other discounts. With dynamic discounts, merchants don't have to repeat certain conditions every time they want to create a new discount. - -The discount generator plugin allows merchants and developers to generate new discounts from a dynamic discount either using the `/discount-code` API Route or the `DiscountGeneratorService`. - ---- - -## Install the Discount Generator Plugin - -To install the Discount Generator plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-discount-generator -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -```js title="medusa-config.js" -const plugins = [ - // ... - { - resolve: `medusa-plugin-discount-generator`, - }, -] -``` - ---- - -## Test the Plugin - - - -- Dynamic discount. Create it using either the [Medusa Admin](!user-guide!/discounts/create) or the [Admin API routes](https://docs.medusajs.com/api/admin#discounts_postdiscounts). - - - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `POST` request to the `/discount-code` API Route: - -```bash apiTesting testApiUrl="http://localhost:9000/discount-code/" testApiMethod="POST" testBodyParams={{"discount_code": "TEST"}} -curl -X POST http://localhost:9000/discount-code/ \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "discount_code": "TEST" - }' -``` - -The API Route accepts in the request body the parameter `discount_code` which is a string indicating the code of the dynamic discount to generate a new discount from. - -The API Route then creates the new discount from the dynamic discount and returns it in the response. - ---- - -## Use DiscountGeneratorService - -Use the `DiscountGeneratorService` to generate a discount in other resources. - -For example: - -```ts title="src/api/store/generate-discount-code/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/medusa" - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - // skipping validation for simplicity - const { dynamicCode } = req.body - const discountGenerator = req.scope.resolve( - "discountGeneratorService" - ) - const code = - await discountGenerator.generateDiscount(dynamicCode) - - res.json({ - code, - }) -} -``` - -The `DiscountGeneratorService` has the method `generateDiscount`. It accepts the code of a dynamic discount as a parameter and creates a new discount having the same attributes as the dynamic discount, but with a different, random code. diff --git a/www/apps/resources/app/_plugins/other/ip-lookup/page.mdx b/www/apps/resources/app/_plugins/other/ip-lookup/page.mdx deleted file mode 100644 index e5c1b41816..0000000000 --- a/www/apps/resources/app/_plugins/other/ip-lookup/page.mdx +++ /dev/null @@ -1,154 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `IP Lookup (ipstack) Plugin`, -} - -# {metadata.title} - -## Features - -Location detection in a commerce store is essential for multi-region support. - -Medusa provides an IP Lookup plugin that integrates the application with [ipstack](https://ipstack.com/) to detect a customer’s location and region. - ---- - -## Install the IP Lookup Plugin - - - -- [ipstack account](https://ipstack.com/) - - - -To install the IP Lookup plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-ip-lookup -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "access_token", "The ipstack account’s access key"], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // other plugins... - { - resolve: `medusa-plugin-ip-lookup`, - options: { - access_token: process.env.IPSTACK_ACCESS_KEY, - }, - }, -] -``` - -### IP Lookup Plugin Options - - - - - Option - Description - - - - - - - `access_token` - - - - - A string indicating the ipstack account’s access key. - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -IPSTACK_ACCESS_KEY= -``` - ---- - -## Test the Plugin - -The plugin provides two resources: the `IpLookupService` and the `preCartCreation` middleware. - - - -Due to how Express resolves the current IP when accessing your website from `localhost`, you won’t be able to test the plugin locally. You can either use tools like ngrok to expose the `9000` port to be accessed publicly, or you have to test it on a deployed Medusa application. - - - -### IpLookupService - -The `IpLookupService` has a method `lookupIp` that accepts the IP address as a parameter, sends a request to ipstack’s API, and returns the retrieved result. - -For example, you can use it in a custom API route: - -```ts title="src/api/store/customer-region/route.ts" -import type { - MedusaRequest, - MedusaResponse, - RegionService, -} from "@medusajs/medusa" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const ipLookupService = req.scope.resolve("ipLookupService") - const regionService = - req.scope.resolve("regionService") - - const ip = - req.headers["x-forwarded-for"] || req.socket.remoteAddress - - const { data } = await ipLookupService.lookupIp(ip) - - if (!data.country_code) { - throw new Error("Couldn't detect country code.") - } - - const region = await regionService.retrieveByCountryCode( - data.country_code - ) - - res.json({ - region, - }) -} -``` - -### preCartCreation - -The `preCartCreation` middleware can be added as a middleware to any route to attach the region ID to that route based on the user’s location. - -For example, you can attach it to all `/store` routes to ensure the customer’s region is always detected: - -```ts title="src/api/middlewares.ts" -import type { MiddlewaresConfig } from "@medusajs/medusa" -const { preCartCreation } = require( - "medusa-plugin-ip-lookup/api/medusa-middleware" -).default - -export const config: MiddlewaresConfig = { - routes: [ - { - matcher: "/store/*", - middlewares: [preCartCreation], - }, - ], -} -``` diff --git a/www/apps/resources/app/_plugins/other/restock-notifications/page.mdx b/www/apps/resources/app/_plugins/other/restock-notifications/page.mdx deleted file mode 100644 index c131bc3826..0000000000 --- a/www/apps/resources/app/_plugins/other/restock-notifications/page.mdx +++ /dev/null @@ -1,189 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Restock Notifications Plugin`, -} - -# {metadata.title} - -## Features - -Customers browsing your products may find something that they need, but it's out of stock. In this scenario, you can keep them interested in your product by notifying them when the product is back in stock. - -The Restock Notifications plugin provides new API Routes to subscribe customers to restock notifications of a specific product variant. It also triggers the `restock-notification.restocked` event whenever a product variant's stock quantity is above a specified threshold. - -However, this plugin doesn't actually implement the sending of the notification, only the required implementation to trigger restock events and allow customers to subscribe to product variants' stock status. To send the notification, use a Notification plugin. - ---- - -## Install the Restock Notifications Plugin - -To install the Restock Notifications plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-restock-notification -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -```js title="medusa-config.js" -const plugins = [ - // other plugins... - { - resolve: `medusa-plugin-restock-notification`, - options: { - // optional options - }, - }, -] -``` - -### Restock Notifications Plugin Options - - - - - Option - Description - Default - - - - - - - `trigger_delay` - - - - - A number indicating the time in milliseconds to delay the triggering of the `restock-notification.restocked` event. - - - - - `0` - - - - - - - `inventory_required` - - - - - A number indicating the minimum inventory quantity to consider a product variant as restocked. - - - - - `0` - - - - -
- -### Run Migrations - -The plugin requires changes in the database. So, before using it, run the `migrations` command: - -```bash -npx medusa migrations run -``` - ---- - -## Test the Plugin - - - -- An out-of-stock product variant. You can edit a variant's stock quantity for testing either using the [Medusa Admin](!user-guide!/products/manage) or the [Admin API Routes](https://docs.medusajs.com/api/admin#products_postproductsproductvariantsvariant). - - - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `POST` request to the API Route `/restock-notifications/variants/{variant_id}` to subscribe to restock notifications of a product variant ID: - -```bash apiTesting testApiUrl="http://localhost:9000/restock-notifications/variants/{variant_id}" testApiMethod="POST" testPathParams={{"variant_id": "variant_01G1G5V2MRX2V3PVSR2WXYPFB6"}} testBodyParams={{"email": "example@gmail.com"}} -curl -X POST http://localhost:9000/restock-notifications/variants/variant_01G1G5V2MRX2V3PVSR2WXYPFB6 \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "email": "example@gmail.com" - }' -``` - -The API Route accepts the following request body parameters: - -1. `email`: a string indicating the email that is subscribing to the product variant's restock notification. -2. `sales_channel_id`: an optional string indicating the ID of the sales channel to check the stock quantity in when subscribing. - -After subscribing to the out-of-stock variant, change its stock quantity to the minimum inventory required to test the event trigger. The new stock quantity should be any value above `0` if you didn't set the `inventory_required` option. - -{/* [Medusa Admin](../../user-guide/products/manage.mdx#manage-product-variants) */} - -You can use the Medusa Admin or the [Admin API Routes](https://docs.medusajs.com/api/admin#products_postproductsproductvariantsvariant) to update the quantity. - -After you update the quantity, the `restock-notification.restocked` is emitted. - ---- - -## Example: Implement Notification Sending with SendGrid - - - -The SendGrid plugin already listens to and handles the `restock-notification.restocked` event. So, if you install it, you don't need to manually create a subscriber that handles this event as explained here. This example is only provided for reference on how to send a notification to the customer using a Notification plugin. - - - -Here's an example of a subscriber that listens to the `restock-notification.restocked` event and uses the [SendGrid plugin](../../notifications/sendgrid/page.mdx) to send the subscribed customers an email: - -```ts title="src/subscribers/restock-notification.ts" -import { - type SubscriberConfig, - type SubscriberArgs, - ProductVariantService, -} from "@medusajs/medusa" - -export default async function handleRestockNotification({ - data, - container, -}: SubscriberArgs>) { - const sendgridService = container.resolve("sendgridService") - const productVariantService: ProductVariantService = - container.resolve("productVariantService") - - // retrieve variant - const variant = await productVariantService.retrieve( - data.variant_id - ) - - sendgridService.sendEmail({ - templateId: "restock-notification", - from: "hello@medusajs.com", - to: data.emails, - dynamic_template_data: { - // any data necessary for your template... - variant, - }, - }) -} - -export const config: SubscriberConfig = { - event: "restock-notification.restocked", -} -``` - -The handler function receives in the `data` property of the first parameter the following properties: - -- `variant_id`: The ID of the variant that has been restocked. -- `emails`: An array of strings indicating the email addresses subscribed to the restocked variant. - -In the handler function, you retrieve the variant by its ID using the `ProductVariantService`, then send the email using the `SendGridService`. diff --git a/www/apps/resources/app/_plugins/other/wishlist/page.mdx b/www/apps/resources/app/_plugins/other/wishlist/page.mdx deleted file mode 100644 index 3cea93219e..0000000000 --- a/www/apps/resources/app/_plugins/other/wishlist/page.mdx +++ /dev/null @@ -1,110 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Wishlist Plugin`, -} - -# {metadata.title} - -## Features - -A wishlist allows customers to save items they like so they can browse and purchase them later. - -Medusa's Wishlist plugin provides the following features: - -- Allow a customer to manage their wishlist, including adding or deleting items. -- Allow a customer to share their wishlist with others using a token. - -Items in the wishlist are added as line items. This allows you to implement functionalities like moving an item from the wishlist to the cart. - ---- - -## Install the Wishlist Plugin - -To install the Wishlist plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-wishlist -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -```js title="medusa-config.js" -const plugins = [ - // ... - { - resolve: `medusa-plugin-wishlist`, - }, -] -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -The plugin exposes four API Routes. - -### Add Item to Wishlist API Route - -The `POST` API Route at `/store/customers/{customer_id}/wishlist` allows customers to add items to their existing or new wishlist: - -```bash apiTesting testApiUrl="http://localhost:9000/store/customers/{customer_id}/wishlist" testApiMethod="POST" testPathParams={{"customer_id": "cus_01G2SG30J8C85S4A5CHM2S1NS2"}} testBodyParams={{"variant_id": "variant_01G1G5V2MRX2V3PVSR2WXYPFB6"}} -curl -X POST http://localhost:9000/store/customers/cus_01G2SG30J8C85S4A5CHM2S1NS2/wishlist \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "variant_id": "variant_01G1G5V2MRX2V3PVSR2WXYPFB6" - }' -``` - -It accepts the following body parameters: - -- `variant_id`: a string indicating the ID of the product variant to add to the wishlist. -- `quantity`: (optional) a number indicating the quantity of the product variant. -- `metadata`: (optional) any metadata to attach to the wishlist item. - -The request returns the full customer object. The wishlist is available in the `customer.metadata.wishlist` property, where its value is an array of items. - -### Delete Item from Wishlist API Route - -The `DELETE` API Route at `/store/customers/{customer_id}/wishlist` allows customers to delete items from their wishlist: - -```bash apiTesting testApiUrl="http://localhost:9000/store/customers/{customer_id}/wishlist" testApiMethod="DELETE" testPathParams={{"customer_id": "cus_01G2SG30J8C85S4A5CHM2S1NS2"}} testBodyParams={{"index": 1}} -curl -X DELETE http://localhost:9000/store/customers/cus_01G2SG30J8C85S4A5CHM2S1NS2/wishlist \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "index": 1 - }' -``` - -The API Route accepts one request body parameter `index`, which indicates the index of the item in the `customer.metadata.wishlist` array. - -The request returns the full customer object. The wishlist is available in the `customer.metadata.wishlist` property, where its value is an array of items. - -#### Generate Share Token API Route - -The `POST` API Route at `/store/customers/{customer_id}/wishlist/share-token` allows customers to retrieve a token that can be used to share the wishlist: - -```bash apiTesting testApiUrl="http://localhost:9000/store/customers/{customer_id}/wishlist/share-token" testApiMethod="POST" testPathParams={{"customer_id": "cus_01G2SG30J8C85S4A5CHM2S1NS2"}} -curl -X POST http://localhost:9000/store/customers/cus_01G2SG30J8C85S4A5CHM2S1NS2/wishlist/share-token -``` - -The request returns an object in the response having the property `share_token`, being the token that can be used to access the wishlist. - -#### Access Wishlist with Token API Route - -The `GET` API Route at `/wishlists/{token}` allows anyone to access the wishlist using its token, where `{token}` is the token retrieved from the [Generate Share Token API Route](#generate-share-token-api-token): - -```bash apiTesting testApiUrl="http://localhost:9000/wishlists/{token}" testApiMethod="GET" testPathParams={{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}} -curl http://localhost:9000/wishlists/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 -``` - -The request returns an object in the response having the following properties: - -- `items`: an array of objects, each being an item in the wishlist. -- `first_name`: a string indicating the first name of the customer that this wishlist belongs to. diff --git a/www/apps/resources/app/_plugins/page.mdx b/www/apps/resources/app/_plugins/page.mdx deleted file mode 100644 index 4827559c69..0000000000 --- a/www/apps/resources/app/_plugins/page.mdx +++ /dev/null @@ -1,11 +0,0 @@ -import { ChildDocs } from "docs-ui" - -export const metadata = { - title: `Plugins`, -} - -# {metadata.title} - -This section includes documentation for official Medusa plugins. You can find community plugins in the [Plugins Library](https://medusajs.com/plugins/) - - \ No newline at end of file diff --git a/www/apps/resources/app/_plugins/payment/klarna/page.mdx b/www/apps/resources/app/_plugins/payment/klarna/page.mdx deleted file mode 100644 index 99094d17d1..0000000000 --- a/www/apps/resources/app/_plugins/payment/klarna/page.mdx +++ /dev/null @@ -1,271 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Klarna Plugin`, -} - -# {metadata.title} - -## Features - -[Klarna](https://www.klarna.com/) is a payment provider that allows customers to pay in different ways including direct payment, installment payments, payment after delivery, and more. - ---- - -## Install the Klarna Plugin - - - -- [Klarna business account](https://slack.com) - - - -To install the Klarna plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-payment-klarna -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "backend_url", "The Klarna URL."], - ["7", "url", "The base Klarna URL based on your environment."], - ["8", "user", "The Klarna Merchant ID (MID)."], - ["9", "password", "The string associated with the Klarna Merchant ID (MID) used for authorization."], - ["10", "merchant_urls", "The merchant URLs to use for orders."], - ["15", "payment_collection_urls", "The merchant URLs to use for payment collections."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-payment-klarna`, - options: { - backend_url: process.env.KLARNA_BACKEND_URL, - url: process.env.KLARNA_URL, - user: process.env.KLARNA_USER, - password: process.env.KLARNA_PASSWORD, - merchant_urls: { - terms: process.env.KLARNA_MERCHANT_TERMS_URL, - checkout: process.env.KLARNA_MERCHANT_CHECKOUT_URL, - confirmation: process.env.KLARNA_MERCHANT_CONFIRMATION_URL, - }, - payment_collection_urls: { - terms: process.env.KLARNA_PAYCOL_TERMS_URL, - checkout: process.env.KLARNA_PAYCOL_CHECKOUT_URL, - confirmation: process.env.KLARNA_PAYCOL_CONFIRMATION_URL, - }, - }, - }, -] -``` - -### Klarna Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `backend_url` - - - - - A string indicating the Klarna URL. - - - - - Yes - - - - - \- - - - - - - - `url` - - - - - A string indicating the [base Klarna URL based on your environment](https://docs.klarna.com/api/api-urls/). - - - - - Yes - - - - - \- - - - - - - - `user` - - - - - The [Klarna Merchant ID (MID)](https://www.klarna.com/us/business/merchant-support/what-is-a-merchant-id/). - - - - - Yes - - - - - \- - - - - - - - `password` - - - - - The string associated with the Klarna Merchant ID (MID) used for [API authorization](https://docs.klarna.com/api/authentication/). - - - - - Yes - - - - - \- - - - - - - - `merchant_urls` - - - - - An object of merchant URLs passed to [Klarna's APIs](https://docs.klarna.com/api/payments/#operation/createCreditSession) for orders. It accepts the following keys: - - - `terms`: The terms URL. - - `checkout`: The checkout URL. - - `confirmation`: The confirmation URL. - - - - - Yes - - - - - \- - - - - - - - `payment_collection_urls` - - - - - An object of merchant URLs passed to [Klarna's APIs](https://docs.klarna.com/api/payments/#operation/createCreditSession) for payment collections. It accepts the following keys: - - - `terms`: The terms URL. - - `checkout`: The checkout URL. - - `confirmation`: The confirmation URL. - - - - - Yes - - - - - \- - - - - - - - `language` - - - - - A string indicating [Klarna's locale](https://docs.klarna.com/klarna-payments/in-depth-knowledge/puchase-countries-currencies-locales/#data-mapping). - - - - - No - - - - - `en-US` - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -KLARNA_BACKEND_URL= -KLARNA_URL= -KLARNA_USER= -KLARNA_PASSWORD= -KLARNA_MERCHANT_TERMS_URL= -KLARNA_MERCHANT_CHECKOUT_URL= -KLARNA_MERCHANT_CONFIRMATION_URL= -KLARNA_PAYCOL_TERMS_URL= -KLARNA_PAYCOL_CHECKOUT_URL= -KLARNA_PAYCOL_CONFIRMATION_URL= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, you must enable the Klarna Payment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers), or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionpaymentproviders). - -Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). You can use Klarna during checkout and to process the order's payment. diff --git a/www/apps/resources/app/_plugins/payment/paypal/page.mdx b/www/apps/resources/app/_plugins/payment/paypal/page.mdx deleted file mode 100644 index 57ab0f92c0..0000000000 --- a/www/apps/resources/app/_plugins/payment/paypal/page.mdx +++ /dev/null @@ -1,354 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `PayPal Plugin`, -} - -# {metadata.title} - -## Features - -[PayPal](https://www.paypal.com) is a payment processor used by millions around the world. It allows customers to purchase orders from your website using their PayPal account rather than the need to enter their card details. - -As a developer, you can use PayPal’s SDKs and APIs to integrate PayPal as a payment method into your ecommerce store. You can test out the payment method in sandbox mode before going live with it as a payment method. - ---- - -## Install the PayPal Plugin - - - -- [PayPal account](https://www.paypal.com). -- [PayPal developer account](https://developer.paypal.com). -- [PayPal client ID and secret](https://developer.paypal.com/api/rest/). -- For deployed Medusa applications, a [PayPal webhook ID](https://developer.paypal.com/api/rest/webhooks/rest/). When creating the Webhook, set the value to `{medusa_url}/paypal/hooks`, where `{medusa_url}` with the URL to your deployed Medusa application. - - - -To install the PayPal plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-payment-paypal -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "clientId", "The PayPal client ID."], - ["7", "clientSecret", "The PayPal client secret."], - ["8", "sandbox", "Whether to use sandbox mode."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-payment-paypal`, - options: { - clientId: process.env.PAYPAL_CLIENT_ID, - clientSecret: process.env.PAYPAL_CLIENT_SECRET, - sandbox: process.env.PAYPAL_SANDBOX, - }, - }, -] -``` - -### Klarna Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `clientId` - - - - - A string indicating the [PayPal client ID](https://developer.paypal.com/api/rest/). - - - - - Yes - - - - - \- - - - - - - - `clientSecret` - - - - - A string indicating the [PayPal client secret](https://developer.paypal.com/api/rest/). - - - - - Yes - - - - - \- - - - - - - - `sandbox` - - - - - A boolean indicating whether to use sandbox mode. Enabling this is useful for testing. - - - - - No - - - - - `false` - - - - - - - `authWebhookId` - - - - - A string indicating the PayPal webhook ID. This is only useful for deployed Medusa applications. - - - - - No - - - - - \- - - - - - - - `capture` - - - - - A boolean indicating whether to automatically capture payments when an order is placed. - - - - - No - - - - - `false`. Payments are authorized when an order is placed and the admin user captures the payment manually. - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -PAYPAL_SANDBOX=true -PAYPAL_CLIENT_ID= -PAYPAL_CLIENT_SECRET= -``` - ---- - -## Test the PayPal Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, you must enable the PayPal Payment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers), or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionpaymentproviders). - -Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). You can use PayPal during checkout and to process the order's payment. - ---- - -## Storefront Setup - -This section provides an example of how to add PayPal as a payment method in custom storefronts. For the Next.js storefront, refer to [this guide](../../../nextjs-starter/page.mdx#paypal-integration) - -### Integration Steps Overview - -1. Show PayPal’s button if the PayPal processor is available for the current cart. -2. When the button is clicked, open PayPal’s payment portal and wait for the customer to authorize the payment. -3. If the payment is authorized successfully, set PayPal’s Payment Sessionas the session used to perform the payment for the current cart, then update the Payment Session on the backend with the data received from PayPal’s payment portal. This data is essential to the backend to verify the authorization and perform additional payment processing later such as capturing payment. -4. Complete the cart to create the order. - -### Add to Custom Storefront - - - -This example assumes your storefront uses React. If not, the steps generally clarify how to implement it in your storefront. - - - -In your storefront, install the [PayPal React components library](https://www.npmjs.com/package/@paypal/react-paypal-js) and the [Medusa JS Client library](https://www.npmjs.com/package/@medusajs/medusa-js): - -```bash npm2yarn -npm install @paypal/react-paypal-js @medusajs/medusa-js -``` - -Then, add the Client ID as an environment variable based on the framework you’re using. - -Next, create the file that holds the PayPal component with the following content: - -export const storefrontHighlights = [ - ["10", "", "Initialize the Medusa JS Client."], - ["16", "", "Retrieve the cart."], - ["18", "handlePayment", "Initialize the payment authorization using `actions.order.authorize()` and takes the customer to authorize the payment with PayPal in another page."], - ["28", "setPaymentSession", "Select the PayPal provider's payment session for the cart."], - ["40", "updatePaymentSession", "Update the payment session's data with the authorization data from PayPal."], - ["52", "complete", "Complete the cart and place the order."], - ["71", `""`, "The PayPal client ID."], - ["81", "PayPalButtons", "Render a PayPal button that, when clicked, initializes the payment using PayPal."] -] - -```tsx highlights={storefrontHighlights} -import { - PayPalButtons, - PayPalScriptProcessor, -} from "@paypal/react-paypal-js" -import { useEffect, useState } from "react" - -import Medusa from "@medusajs/medusa-js" - -function Paypal() { - const client = new Medusa({ - baseUrl: "http://localhost:9000", - maxRetries: 3, - }) - const [errorMessage, setErrorMessage] = useState(undefined) - const [processing, setProcessing] = useState(false) - const cart = "..." // TODO retrieve the cart here - - const handlePayment = (data, actions) => { - actions.order.authorize().then(async (authorization) => { - if (authorization.status !== "COMPLETED") { - setErrorMessage( - `An error occurred, status: ${authorization.status}` - ) - setProcessing(false) - return - } - - const response = await client.carts.setPaymentSession( - cart.id, - { - provider_id: "paypal", - } - ) - - if (!response.cart) { - setProcessing(false) - return - } - - await client.carts.updatePaymentSession( - cart.id, - "paypal", - { - data: { - data: { - ...authorization, - }, - }, - } - ) - - const { data, type } = await client.carts.complete( - cart.id - ) - - if (!data || type !== "order") { - setProcessing(false) - return - } - - // order successful - alert("success") - }) - } - - return ( -
- {cart !== undefined && ( - ", - currency: "EUR", - intent: "authorize", - }} - > - {errorMessage && ( - - {errorMessage} - - )} - - - )} -
- ) -} - -export default Paypal -``` - -A brief overview of what this component does: - -1. You initialize the Medusa JS Client. -2. You retrieve the cart. Ideally, the cart should be managed through a context. So, every time the cart has been updated the cart should be updated in the context to be accessed from all components. -3. You render a PayPal button that, when clicked, initializes the payment using PayPal. You use the components from the PayPal React components library to render the button and you pass the `PayPalScriptProcessor` component the Client ID. Make sure to replace `` with the environment variable you added. -4. When the button is clicked, the `handlePayment` function is executed. In this method, you initialize the payment authorization using `actions.order.authorize()`. It takes the customer to another page to log in with PayPal and authorize the payment. -5. After the payment is authorized successfully on PayPal’s portal, the fulfillment function passed to `actions.order.authorize().then` is executed. -6. In the fulfillment function, you select the PayPal provider's [payment session in the cart](https://docs.medusajs.com/api/store#carts_postcartscartpaymentsession). Then, you [update the payment session](https://docs.medusajs.com/api/store#carts_postcartscartpaymentsessionupdate)'s data in the Medusa application with the authorization data received from PayPal. -7. You [complete the cart and place the order](https://docs.medusajs.com/api/store#carts_postcartscartcomplete). If successful, you just show a success alert. - -You can then import this component where you want to show it in your storefront. - -If you run the Medusa application and the storefront, you can use the PayPal button during checkout. diff --git a/www/apps/resources/app/_plugins/payment/stripe/page.mdx b/www/apps/resources/app/_plugins/payment/stripe/page.mdx deleted file mode 100644 index 5bbec82c67..0000000000 --- a/www/apps/resources/app/_plugins/payment/stripe/page.mdx +++ /dev/null @@ -1,447 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Stripe Plugin`, -} - -# {metadata.title} - -## Features - -[Stripe](https://stripe.com/) is a battle-tested and unified platform for transaction handling. Stripe supplies you with the technical components needed to handle transactions safely and all the analytical features necessary to gain insight into your sales. These features are also available in a safe test environment which allows for a concern-free development process. - ---- - -## Install the PayPal Plugin - - - -- [Stripe account](https://stripe.com). -- [Stripe API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) -- For deployed Medusa applications, a [Stripe webhook secret](https://docs.stripe.com/webhooks#add-a-webhook-endpoint). When creating the Webhook, set the endpoint URL to `{medusa_url}/stripe/hooks`, where `{medusa_url}` with the URL to your deployed Medusa application. - - - -To install the Stripe plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-payment-stripe -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "api_key", "The Stripe API key."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-payment-stripe`, - options: { - api_key: process.env.STRIPE_API_KEY, - }, - }, -] -``` - -### Stripe Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `api_key` - - - - - A string indicating the [Stripe API key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard). - - - - - Yes - - - - - \- - - - - - - - `webhook_secret` - - - - - A string indicating the [Stripe webhook secret](https://docs.stripe.com/webhooks#add-a-webhook-endpoint). This is only useful for deployed Medusa applications. - - - - - No - - - - - \- - - - - - - - `capture` - - - - - A boolean indicating whether to automatically capture payments when an order is placed. - - - - - No - - - - - `false`. Payments are authorized when an order is placed and the admin user captures the payment manually. - - - - - - - `automatic_payment_methods` - - - - - A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you're integrating services like Apple pay or Google pay. - - - - - No - - - - - `false` - - - - - - - `payment_description` - - - - - A string used as the default description of a payment if none is available in `cart.context.payment_description`. - - - - - No - - - - - \- - - - - - - - `webhook_delay` - - - - - A number indicating the delay in milliseconds before processing the webhook event. - - - - - No - - - - - `5000` (five seconds) - - - - - - - `webhook_retries` - - - - - A number of times to retry the webhook event processing in case of an error. - - - - - No - - - - - `3` - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -STRIPE_API_KEY= -``` - ---- - -## Test the Stripe Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, you must enable the Stripe Payment Provider in at least one region to use it. You can do that using either the [Medusa Admin](!user-guide!/settings/regions/providers), or the [Admin API Routes](https://docs.medusajs.com/api/admin#regions_postregionsregionpaymentproviders). - -Finally, try to place an order using either a [storefront](../../../nextjs-starter/page.mdx) or the [Store API Routes](https://docs.medusajs.com/api/store). You can use Stripe during checkout and to process the order's payment. - ---- - -## Webhook Events - -This plugin handles the following Stripe webhook events: - -- `payment_intent.succeeded`: If the payment is associated with a payment collection, the plugin captures the payments within the webhook listener of this event. Otherwise, it checks first if an order is created and, if not, completes the cart which creates the order. It also captures the payment of the order associated with the cart if it's not captured already. -- `payment_intent.amount_capturable_updated`: If no order is created for the cart associated with the payment, the webhook listener completes the cart and creates the order. -- `payment_intent.payment_failed`: the webhook listener prints the error message received from Stripe into the logs. - ---- - -## Storefront Setup - -This section provides an example of how to add Stripe as a payment method in custom storefronts. For the Next.js storefront, refer to [this guide](../../../nextjs-starter/page.mdx#stripe-integration) - -### Integration Steps Overview - -1. When the user reaches the payment section during checkout, [create payment sessions](https://docs.medusajs.com/api/store#carts_postcartscartpaymentsessions). -2. If the user chooses Stripe, select the Stripe provider's [the payment session](https://docs.medusajs.com/api/store#carts_postcartscartpaymentsession) in the cart. -3. After the user enters their card details and submits the form, confirm the payment with Stripe. -4. If successful, [complete the cart](https://docs.medusajs.com/api/store#carts_postcartscartcomplete) in Medusa. - -### Add to Custom Storefront - - - -This example assumes your storefront uses React. If not, the steps generally clarify how to implement it in your storefront. - - - -In your storefront, install [Stripe's React and JavaScript libraries](https://docs.stripe.com/stripe-js/react) and the [Medusa JS Client library](https://www.npmjs.com/package/@medusajs/medusa-js): - -```bash npm2yarn -npm install --save @stripe/react-stripe-js @stripe/stripe-js @medusajs/medusa-js -``` - -Then, add [Stripe's publishable key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) as an environment variable based on the framework you’re using. - -After that, create a container component that holds the payment card component: - -export const containerHighlights = [ - ["6", `""`, "Stripe's publishable key."] -] - -```tsx -import { useState } from "react" -import { Elements } from "@stripe/react-stripe-js" -import Form from "./Form" -import { loadStripe } from "@stripe/stripe-js" - -const stripePromise = loadStripe("") - -export default function Container() { - const [clientSecret, setClientSecret] = useState() - - // TODO set clientSecret - - return ( -
- {clientSecret && ( - -
- - )} -
- ) -} -``` - -In this component, you use Stripe’s `loadStripe` function outside of the component’s implementation to ensure that Stripe doesn’t re-load with every change. The function accepts Stripe's publishable key. - -Then, inside the component’s implementation, you add a state variable `clientSecret` which you’ll retrieve in the next section. - -The `Elements` Stripe component wraps a `Form` component that you’ll create next. The `Elements` component allows child elements to get access to the card’s inputs and their data using Stripe’s `useElements` hook. - -Next, create a new file for the `Form` component with the following content: - -```tsx -import { - CardElement, - useElements, - useStripe, -} from "@stripe/react-stripe-js" - -export default function Form({ clientSecret, cartId }) { - const stripe = useStripe() - const elements = useElements() - - async function handlePayment(e) { - e.preventDefault() - // TODO handle payment - } - - return ( - - - - - ) -} -``` - -The `useStripe` hook gives you access to the stripe instance to confirm the payment later. The `useElements` hook gives you access to the card element to retrieve the entered card details safely. - -You’ll now implement the integration steps explained earlier in the `Container` component. - -Start by initializing the Medusa client: - -```tsx -import Medusa from "@medusajs/medusa-js" - -export default function Container() { - const client = new Medusa({ - baseUrl: "http://localhost:9000", - maxRetries: 3, - }) - // ... -} -``` - -Then, in the place of the `//TODO`, initialize the payment sessions and create a payment session if Stripe is available: - -```tsx -client.carts.createPaymentSessions(cart.id).then(({ cart }) => { - // check if stripe is selected - const isStripeAvailable = cart.payment_sessions?.some( - (session) => session.provider_id === "stripe" - ) - if (!isStripeAvailable) { - return - } - - // select stripe payment session - client.carts - .setPaymentSession(cart.id, { - provider_id: "stripe", - }) - .then(({ cart }) => { - setClientSecret(cart.payment_session.data.client_secret) - }) -}) - -``` - - - -It’s assumed you have access to the `cart` object throughout your storefront. Ideally, the `cart` should be managed through a context. In that case, you probably wouldn’t need a `clientSecret` state variable as you can use the client secret directly from the `cart` object. - - - -Once the client secret is set, the form is shown to the user. - -The last step in the integration step is confirming the payment with Stripe and if it’s done successfully, completing the customer's order. - -In the `Form` component, initialize the Medusa client or re-use the same client in the `Container` element - -```tsx -import Medusa from "@medusajs/medusa-js" - -export default function Form() { - const client = new Medusa({ - baseUrl: "http://localhost:9000", - maxRetries: 3, - }) - // ... -} -``` - -Then, replace the `//TODO` in the `handlePayment` function with the following content: - -```jsx -return stripe.confirmCardPayment(clientSecret, { - payment_method: { - card: elements.getElement(CardElement), - billing_details: { - name, - email, - phone, - address: { - city, - country, - line1, - line2, - postal_code, - }, - }, - }, -}).then(({ error, paymentIntent }) => { - // TODO handle errors - client.carts.complete(cartId).then( - (resp) => console.log(resp) - ) -}) -``` - -You use the `confirmCardPayment` method in the `stripe` object passing it the client secret, which you can access in the cart object if it’s available through a context. - -This method also requires as a second parameter an object of the customer’s information including `name`, `email`, and their address. - -Once the promise resolves you handle any errors that could've occurred. If no errors occurred, you complete the customer’s order. - -If you run the Medusa application and the storefront, you can use Stripe during checkout. diff --git a/www/apps/resources/app/_plugins/search/algolia/page.mdx b/www/apps/resources/app/_plugins/search/algolia/page.mdx deleted file mode 100644 index 52c89ec2f8..0000000000 --- a/www/apps/resources/app/_plugins/search/algolia/page.mdx +++ /dev/null @@ -1,232 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Algolia Plugin`, -} - -# {metadata.title} - -## Features - -[Algolia](https://www.algolia.com/) is a search engine service that allows developers to integrate advanced search functionalities into their websites including typo tolerance, recommended results, and quick responses. - -Algolia is used for a wide range of use cases, including commerce stores. By integrating Algolia into your commerce application, you provide your customers with a better user experience and help them find what they’re looking for swifltly. - ---- - -## Install the Algolia Plugin - - - -- [Algolia account](https://www.algolia.com/users/sign_up) -- [Algolia app ID](https://support.algolia.com/hc/en-us/articles/11040113398673-Where-can-I-find-my-application-ID-and-the-index-name) -- [Algolia API key](https://support.algolia.com/hc/en-us/articles/11972559809681-How-do-I-find-my-Admin-API-key) - - - -To install the Algolia plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-algolia -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "applicationId", "The Algolia app ID."], - ["7", "adminApiKey", "The Algolia API key."], - ["8", "settings", "Settings of indices created in Algolia."], - ["9", "products", "The name of an index to create. In this example, it's `products`."], - ["10", "indexSettings", "The settings of the index."], - ["11", "searchableAttributes", "The attributes that can be searched in the index."], - ["12", "attributesToRetrieve", "The attributes to retrieve in the search results."], - ["26", "transformer", "A function that shapes the object to be indexed."] -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-plugin-algolia`, - options: { - applicationId: process.env.ALGOLIA_APP_ID, - adminApiKey: process.env.ALGOLIA_ADMIN_API_KEY, - settings: { - products: { - indexSettings: { - searchableAttributes: ["title", "description"], - attributesToRetrieve: [ - "id", - "title", - "description", - "handle", - "thumbnail", - "variants", - "variant_sku", - "options", - "collection_title", - "collection_handle", - "images", - ], - }, - transformer: (product) => ({ - objectID: product.id, - // other attributes... - }), - }, - }, - }, - }, -] -``` - -### Algolia Plugin Options - - - - - Option - Description - Required - - - - - - - `applicationId` - - - - - A string indicating the Algolia app ID. - - - - - Yes - - - - - - - `adminApiKey` - - - - - A string indicating the Algolia API key. - - - - - Yes - - - - - - - `settings` - - - - - An object of settings. Its keys are names of indices to create in Algolia (in the example above, `products`), and values are an object of the index's settings. - - - - - No - - - - - - - `settings.[indexName].indexSettings` - - - - - An object of index settings. It accepts two properties: - - - `searchableAttributes`: An array of field names that can be searched. - - `attributesToRetrieve`: An array of field names retrieved in search results. - - - - - If `settings` is provided, this property is required. - - - - - - - `settings.[indexName].transformer` - - - - - A function used to change the shape of the indexed records. For example, you can add details related to variants or custom relations, or filter out certain products. - - The function accepts as a parameter that data model object to index, such as a product, and returns an object to be indexed. - - - - - No - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -ALGOLIA_APP_ID= -ALGOLIA_ADMIN_API_KEY= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `POST` request to the `/store/products/search`: - -```bash apiTesting testApiUrl="http://localhost:9000/store/products/search" testApiMethod="POST" testBodyParams={{"q": "shirt"}} -curl -X POST http://localhost:9000/store/products/search \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "q": "shirt" - }' -``` - -The response contains a `hits` array with the results from the Algolia search engine. - -### Add or Update Products - -If you add or update products in your Medusa application, it'll be reflected in the Algolia indices. - ---- - -## Add Search to your Storefront - -### Next.js Starter - -Refer to the [Next.js Starter guide](../../../nextjs-starter/page.mdx#configure-algolia) to learn how to configure Algolia. - -### Custom Storefront - -To integrate Algolia's search functionalities in your custom storefront, refer to [Algolia's InstantSearch.js documentation](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/). diff --git a/www/apps/resources/app/_plugins/search/meilisearch/page.mdx b/www/apps/resources/app/_plugins/search/meilisearch/page.mdx deleted file mode 100644 index 5432cb4ee9..0000000000 --- a/www/apps/resources/app/_plugins/search/meilisearch/page.mdx +++ /dev/null @@ -1,248 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `MeiliSearch Plugin`, -} - -# {metadata.title} - -## Features - -[MeiliSearch](https://www.meilisearch.com/) is a super-fast, open source search engine built in Rust. It comes with a wide range of features including typo-tolerance, filtering, and sorting. - -MeiliSearch also provides a pleasant developer experience, as it is extremely intuitive and newcomer-friendly. So, even if you're new to the search engine ecosystem, [their documentation](https://docs.meilisearch.com/) is resourceful enough for everyone to go through and understand. - ---- - -## Install the MeiliSearch Plugin - - - -- [MeiliSearch installed](https://docs.meilisearch.com/learn/getting_started/quick_start.html#setup-and-installation) -- [MeiliSearch master key](https://www.meilisearch.com/docs/learn/security/master_api_keys#protecting-a-meilisearch-instance) - - - -To install the Algolia plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-plugin-meilisearch -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "config", "The MeiliSearch connection configuration object."], - ["7", "host", "The MeiliSearch host."], - ["8", "apiKey", "The MeiliSearch master key."], - ["10", "settings", "Settings of indices created in MeiliSearch."], - ["11", "products", "The name of an index to create. In this example, it's `products`."], - ["12", "indexSettings", "The settings of the index."], - ["13", "searchableAttributes", "The attributes that can be searched in the index."], - ["18", "displayedAttributes", "The attributes to retrieve in the search results."], - ["27", "transformer", "A function that shapes the object to be indexed."] -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-plugin-meilisearch`, - options: { - config: { - host: process.env.MEILISEARCH_HOST, - apiKey: process.env.MEILISEARCH_API_KEY, - }, - settings: { - products: { - indexSettings: { - searchableAttributes: [ - "title", - "description", - "variant_sku", - ], - displayedAttributes: [ - "id", - "title", - "description", - "variant_sku", - "thumbnail", - "handle", - ], - }, - transformer: (product) => ({ - id: product.id, - // other attributes... - }), - }, - }, - }, - }, -] -``` - -### MeiliSearch Plugin Options - - - - - Option - Description - Required - - - - - - - `config` - - - - - An object of MeiliSearch connection configurations. It accepts two properties: - - - `host`: A string indicating the MeiliSearch host. For example, `http://127.0.0.1:7700`. - - `apiKey`: A string indicating the [MeiliSearch master key](https://www.meilisearch.com/docs/learn/security/master_api_keys#protecting-a-meilisearch-instance). - - - - - Yes - - - - - - - `settings` - - - - - An object of settings. Its keys are names of indices to create in MeiliSearch (in the example above, `products`), and values are an object of the index's settings. - - - - - No - - - - - - - `settings.[indexName].indexSettings` - - - - - An object of index settings. It accepts two properties: - - - `searchableAttributes`: An array of field names that can be searched. - - `displayedAttributes`: An array of field names retrieved in search results. - - - - - If `settings` is provided, this property is required. - - - - - - - `settings.[indexName].transformer` - - - - - A function used to change the shape of the indexed records. For example, you can add details related to variants or custom relations, or filter out certain products. - - The function accepts as a parameter that data model object to index, such as a product, and returns an object to be indexed. - - - - - No - - - - - - - `settings.[indexName].primaryKey` - - - - - A string indicating which field in the data model acts as a primary key of a document. It's used to enforce unique documents in an index. Learn more in [MeiliSearch's documentation](https://docs.meilisearch.com/learn/core_concepts/primary_key.html#primary-field). - - - - - `id` - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -MEILISEARCH_HOST= -MEILISEARCH_API_KEY= -``` - ---- - -## Test the Plugin - - - -- [MeiliSearch running in the background](https://www.meilisearch.com/docs/learn/getting_started/quick_start#running-meilisearch). - - - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `POST` request to the `/store/products/search`: - -```bash apiTesting testApiUrl="http://localhost:9000/store/products/search" testApiMethod="POST" testBodyParams={{"q": "shirt"}} -curl -X POST http://localhost:9000/store/products/search \ - --header 'Content-Type: application/json' \ - --data-raw '{ - "q": "shirt" - }' -``` - -The response contains a `hits` array with the results from the MeiliSearch search engine. - -### Add or Update Products - -If you add or update products in your Medusa application, it'll be reflected in the MeiliSearch indices. - ---- - -## Add Search to your Storefront - - - -- [MeiliSearch API key](https://www.meilisearch.com/docs/learn/security/master_api_keys#creating-an-api-key). - - - -### Next.js Starter - -Refer to the [Next.js Starter guide](../../../nextjs-starter/page.mdx#configure-meilisearch) to learn how to configure MeiliSearch. - -### Custom Storefront - -To integrate MeiliSearch's search functionalities in your custom storefront, refer to [MeiliSearch's documentation](https://docs.meilisearch.com/learn/what_is_meilisearch/sdks.html#front-end-tools). diff --git a/www/apps/resources/app/_plugins/source/shopify/page.mdx b/www/apps/resources/app/_plugins/source/shopify/page.mdx deleted file mode 100644 index 3fa9cca852..0000000000 --- a/www/apps/resources/app/_plugins/source/shopify/page.mdx +++ /dev/null @@ -1,153 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Shopify Source Plugin`, -} - -# {metadata.title} - -## Features - -If you're migrating from Shopify to Medusa, this plugin facilitates the process. It migrates data related to your products on Shopify to Medusa. - -It also registers a scheduled job that runs periodically and ensures your data is synced between Shopify and Medusa. - ---- - -## Install the Shopify Source Plugin - - - -- [Shopify account](https://accounts.shopify.com/lookup?rid=8ea3d7a5-0bd6-4645-8e1c-ebf19bd750c7) -- [Shopify custom app](https://help.shopify.com/en/manual/apps/app-types/custom-apps) with `read_products` admin API access scope. -- [Shopify API secret key](https://help.shopify.com/en/manual/apps/app-types/custom-apps#get-the-api-credentials-for-a-custom-app) - - - -To install the Shopify Source plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-source-shopify -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "domain", "The Shopify store's subdomain."], - ["7", "password", "The Shopify API secret key."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ..., - { - resolve: "medusa-source-shopify", - options: { - domain: process.env.SHOPIFY_DOMAIN, - password: process.env.SHOPIFY_PASSWORD, - }, - }, -] -``` - -### Shopify Source Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `domain` - - - - - A string indicating the Shopify store's subdomain. Your store's domain is of the format `.myshopify.com`. The `` is the value of this option. - - - - - Yes - - - - - \- - - - - - - - `password` - - - - - A string indicating the [Shopify API secret key](https://help.shopify.com/en/manual/apps/app-types/custom-apps#get-the-api-credentials-for-a-custom-app). - - - - - Yes - - - - - \- - - - - - - - `ignore_threshold` - - - - - The products retrieved from Shopify are added to the cache. This option sets the number of seconds an item can live in the cache before it’s removed. - - - - - No - - - - - `2` - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -SHOPIFY_DOMAIN= -SHOPIFY_PASSWORD= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -This runs the migration script. The products are migrated from Shopify into Medusa. diff --git a/www/apps/resources/app/_plugins/storage/local/page.mdx b/www/apps/resources/app/_plugins/storage/local/page.mdx deleted file mode 100644 index 330b2f1c36..0000000000 --- a/www/apps/resources/app/_plugins/storage/local/page.mdx +++ /dev/null @@ -1,127 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `Local File Storage Plugin`, -} - -# {metadata.title} - -## Features - -The Local File Storage plugin allows you to upload media assets, such as product images, to a local directory. This is useful during development. - - - -For production, it's recommended to use a storage plugin that hosts your images on a third-party service. This storage plugin doesn't handle advanced features such as presigned URLs. - - - ---- - -## Install the Local File Storage Plugin - -To install the Local File Storage plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install @medusajs/file-local -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -```js title="medusa-config.js" -const plugins = [ - // ... - { - resolve: `@medusajs/file-local`, - options: { - // optional - }, - }, -] -``` - -### Local File Storage Plugin Options - - - - - Option - Description - Default - - - - - - - `upload_dir` - - - - - A string indicating the relative path to upload the files to. - - - - - `uploads/images` - - - - - - - `backend_url` - - - - - A string indicating the URL of your Medusa application. This is helpful if you deploy your application or change the port used. - - - - - `http://localhost:9000` - - - - -
- ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, upload a product image either using the [Medusa Admin](!user-guide!/products/manage#manage-product-media) or the [Admin API routes](https://docs.medusajs.com/api/admin#uploads_postuploads). - ---- - -## Next.js Starter Configuration - -If you’re using the [Next.js Starter storefront](../../../nextjs-starter/page.mdx), add the following option to the exported object in `next.config.js`: - -```js title="next.config.js" -const { withStoreConfig } = require("./store-config") - -// ... - -module.exports = withStoreConfig({ - // ... - images: { - domains: [ - // ... - "{medusa_domain}", - ], - }, -}) -``` - -This adds the Medusa application's domain name into the configured images domain names. If you don't add the configuration, you’ll receive the error ["next/image Un-configured Host”](https://nextjs.org/docs/messages/next-image-unconfigured-host). - -Make sure to replace `{medusa_domain}` with the domain of your Medusa application. For example, `localhost`. diff --git a/www/apps/resources/app/_plugins/storage/minio/page.mdx b/www/apps/resources/app/_plugins/storage/minio/page.mdx deleted file mode 100644 index 6c37d05ee9..0000000000 --- a/www/apps/resources/app/_plugins/storage/minio/page.mdx +++ /dev/null @@ -1,296 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `MinIO Plugin`, -} - -# {metadata.title} - -## Features - -[MinIO](https://min.io/) is an open-source object storage server compatible with the Amazon S3 API. It allows users to store photos, videos, backups, and more. - -With the MinIO plugin, you'll benefit from basic and advanced storage functionalities, including public and private uploads, and presigned URLs. - ---- - -## Install the MinIO Plugin - - - -- [Install MinIO](https://min.io/docs/minio/linux/index.html). -- Change port to `9001` using the [console address](https://min.io/docs/minio/linux/reference/minio-server/minio-server.html#minio.server.-console-address) and [address](https://min.io/docs/minio/linux/reference/minio-server/minio-server.html#minio.server.-address) CLI options. -- [MinIO bucket with public access policy](https://min.io/docs/minio/linux/administration/console/managing-objects.html#creating-buckets). -- [MinIO access and secret access key](https://min.io/docs/minio/linux/administration/console/security-and-access.html#id1) - - - -To install the MinIO plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-file-minio -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "bucket", "The bucket to upload files to."], - ["7", "access_key_id", "The MinIO access key."], - ["8", "secret_access_key", "The MinIO secret access key."], - ["9", "endpoint", "The URL of your MinIO server."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-file-minio`, - options: { - bucket: process.env.MINIO_BUCKET, - access_key_id: process.env.MINIO_ACCESS_KEY, - secret_access_key: process.env.MINIO_SECRET_KEY, - endpoint: process.env.MINIO_ENDPOINT, - }, - }, -] -``` - -### MinIO Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `bucket` - - - - - A string indicating the bucket to upload files to. - - - - - Yes - - - - - \- - - - - - - - `access_key_id` - - - - - A string indicating the [MinIO access key](https://min.io/docs/minio/linux/administration/console/security-and-access.html#id1). - - - - - Yes - - - - - \- - - - - - - - `secret_access_key` - - - - - A string indicating the [MinIO secret access key](https://min.io/docs/minio/linux/administration/console/security-and-access.html#id1). - - - - - Yes - - - - - \- - - - - - - - `endpoint` - - - - - A string indicating the URL of your MinIO server. - - - - - Yes - - - - - \- - - - - - - - `private_bucket` - - - - - A string indicating the bucket to use for private media, such as the CSV file of exported products. - - - - - Required if you're using import/export features that require a private bucket. - - - - - \- - - - - - - - `private_access_key_id` - - - - - A string indicating the MinIO access key to use for private uploads. - - - - - No - - - - - The value of `access_key_id`. - - - - - - - `private_secret_access_key` - - - - - A string indicating the MinIO secret access key to use for private uploads. - - - - - No - - - - - The value of `secret_access_key`. - - - - - - - `download_url_duration` - - - - - A number indicating the expiry time of presigned URLs in seconds. - - - - - No - - - - - `60` - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -MINIO_BUCKET= -MINIO_ACCESS_KEY= -MINIO_SECRET_KEY= -MINIO_ENDPOINT= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, upload a product image either using the [Medusa Admin](!user-guide!/products/manage#manage-product-media) or the [Admin API routes](https://docs.medusajs.com/api/admin#uploads_postuploads). - ---- - -## Next.js Starter Template Configuration - -If you’re using the [Next.js Starter storefront](../../../nextjs-starter/page.mdx), add the following option to the exported object in `next.config.js`: - -```jsx title="next.config.js" -const { withStoreConfig } = require("./store-config") - -// ... - -module.exports = withStoreConfig({ - // ... - images: { - domains: [ - // ... - "{minio_domain}", - ], - }, -}) -``` - -This adds the MinIO domain into the configured images domain names. If you don't add the configuration, you’ll receive the error ["next/image Un-configured Host”](https://nextjs.org/docs/messages/next-image-unconfigured-host). - -Make sure to replace `{minio_domain}` with the MinIO domain. For example, `127.0.0.1`. diff --git a/www/apps/resources/app/_plugins/storage/s3/page.mdx b/www/apps/resources/app/_plugins/storage/s3/page.mdx deleted file mode 100644 index 49eca4c45f..0000000000 --- a/www/apps/resources/app/_plugins/storage/s3/page.mdx +++ /dev/null @@ -1,363 +0,0 @@ -import { Table, DetailsList } from "docs-ui" -import AclErrorSection from "../../../troubleshooting/_sections/other/s3-acl.mdx" - -export const metadata = { - title: `S3 Plugin`, -} - -# {metadata.title} - -## Features - -[Amazon S3](https://aws.amazon.com/s3/) is a cloud storage service that offers scalable object storage. It allows users to store and retrieve images, videos, and other media types. - -With the S3 plugin, you'll benefit from basic and advanced storage functionalities, including public and private uploads, and presigned URLs. - ---- - -## Preparations - - - -- [AWS account](https://console.aws.amazon.com/console/home?nc2=h_ct&src=header-signin). -- [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) with the "Public Access setting" enabled. -- [AWS user with AmazonS3FullAccess permissions](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-create-and-attach-iam-policy.html). -- [AWS user access key ID and secret access key](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_CreateAccessKey) - - - -### Bucket Policies - -Change your [bucket's policy](https://docs.aws.amazon.com/AmazonS3/latest/userguide/add-bucket-policy.html) to the following: - -```json -{ - "Version": "2012-10-17", - "Id": "Policy1397632521960", - "Statement": [ - { - "Sid": "Stmt1397633323327", - "Effect": "Allow", - "Principal": { - "AWS": "*" - }, - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::{bucket_name}/*" - } - ] -} -``` - -Make sure to replace `{bucket_name}` with the name of the bucket you created. - ---- - -## Install the S3 Plugin - -To install the MinIO plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-file-s3 -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "bucket", "The bucket to upload files to."], - ["7", "s3_url", "The URL to the bucket."], - ["8", "access_key_id", "The AWS user's access key ID."], - ["9", "secret_access_key", "The AWS user's secret access key."], - ["10", "region", "The bucket's region code."], -] - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-file-s3`, - options: { - bucket: process.env.S3_BUCKET, - s3_url: process.env.S3_URL, - access_key_id: process.env.S3_ACCESS_KEY_ID, - secret_access_key: process.env.S3_SECRET_ACCESS_KEY, - region: process.env.S3_REGION, - }, - }, -] -``` - -### S3 Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `bucket` - - - - - A string indicating the bucket to upload files to. - - - - - Yes - - - - - \- - - - - - - - `s3_url` - - - - - A string indicating the URL to your bucket. It’s in the form `https://.s3..amazonaws.com`, where `` is the name of the bucket and the `` is the region the bucket is created in. - - - - - Yes - - - - - \- - - - - - - - `access_key_id` - - - - - A string indicating the AWS user's access key ID. - - - - - Yes - - - - - \- - - - - - - - `secret_access_key` - - - - - A string indicating the AWS user's secret access key. - - - - - Yes - - - - - \- - - - - - - - `region` - - - - - A string indicating the region code of your bucket. For example, `us-east-1`. - - - - - Yes - - - - - \- - - - - - - - `prefix` - - - - - A string indicating a prefix to apply on stored file names. If supplied, a `/` is added at the end of the prefix. - - - - - No - - - - - \- - - - - - - - `download_file_duration` - - - - - A number indicating the expiry time of presigned URLs in seconds. - - - - - No - - - - - [S3's default expiration time](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html#PresignedUrl-Expiration) - - - - - - - `cache_control` - - - - - A string indicating [how long objects remain in the CloudFront's cache](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html). - - - - - No - - - - - `max-age=31536000` (a year) - - - - - - - `aws_config_object` - - - - - An object of [AWS Configurations](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html) passed to all requests. - - - - - No - - - - - \- - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -S3_BUCKET= -S3_URL= -S3_ACCESS_KEY_ID= -S3_SECRET_ACCESS_KEY= -S3_REGION= -``` - ---- - -## Test the S3 Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, upload a product image either using the [Medusa Admin](!user-guide!/products/manage#manage-product-media) or the [Admin API routes](https://docs.medusajs.com/api/admin#uploads_postuploads). - ---- - -## Next.js Starter Template Configuration - -If you’re using the [Next.js Starter storefront](../../../nextjs-starter/page.mdx), add the following option to the exported object in `next.config.js`: - -```jsx title="next.config.js" -const { withStoreConfig } = require("./store-config") - -// ... - -module.exports = withStoreConfig({ - // ... - images: { - domains: [ - // ... - "{s3_domain}", - ], - }, -}) -``` - -This adds the S3's domain into the configured images domain names. If you don't add the configuration, you’ll receive the error ["next/image Un-configured Host”](https://nextjs.org/docs/messages/next-image-unconfigured-host). - -Make sure to replace `{s3_domain}` with the S3 domain which is of the format `.s3..amazonaws.com`. - ---- - -## Troubleshooting - - - } - ]} -/> diff --git a/www/apps/resources/app/_plugins/storage/spaces/page.mdx b/www/apps/resources/app/_plugins/storage/spaces/page.mdx deleted file mode 100644 index 78389821cb..0000000000 --- a/www/apps/resources/app/_plugins/storage/spaces/page.mdx +++ /dev/null @@ -1,282 +0,0 @@ -import { Table, DetailsList } from "docs-ui" -import AclErrorSection from "../../../troubleshooting/_sections/other/s3-acl.mdx" - -export const metadata = { - title: `Spaces Plugin`, -} - -# {metadata.title} - -## Features - -[DigitalOcean Spaces](https://www.digitalocean.com/products/spaces) is an object storage service provided by DigitalOcean. Spaces is designed to make it easy and cost-effective to store medias such as images, videos, and more. - -With the DigitalOcean plugin, you'll benefit from basic and advanced storage functionalities, including public and private uploads, and presigned URLs. - ---- - -## Install the Spaces Plugin - - - -- [DigitalOcean account](https://cloud.digitalocean.com/registrations/new) -- [DigitalOcean Spaces bucket](https://docs.digitalocean.com/products/spaces/how-to/create/) -- [DigitalOcean Spaces access and secret access keys](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys) - - - -To install the Spaces plugin, run the following command in the directory of your Medusa application: - -```bash npm2yarn -npm install medusa-file-spaces -``` - -Next, add the plugin into the `plugins` array in `medusa-config.js`: - -export const highlights = [ - ["6", "bucket", "The bucket to upload files to."], - ["7", "spaces_url", "Either the Origin Endpoint or the CDN endpoint of your Spaces Object Storage bucket."], - ["8", "access_key_id", "The Spaces access key."], - ["9", "secret_access_key", "The Spaces secret access key."], - ["10", "region", "The region your Spaces Object Storage bucket is in."], - ["11", "endpoint", "The Spaces Origin Endpoint."], -] - -Then, add the following environment variables: - -```js title="medusa-config.js" highlights={highlights} -const plugins = [ - // ... - { - resolve: `medusa-file-spaces`, - options: { - bucket: process.env.SPACE_BUCKET, - spaces_url: process.env.SPACE_URL, - access_key_id: process.env.SPACE_ACCESS_KEY_ID, - secret_access_key: process.env.SPACE_SECRET_ACCESS_KEY, - region: process.env.SPACE_REGION, - endpoint: process.env.SPACE_ENDPOINT, - }, - }, -] -``` - -### Spaces Plugin Options - - - - - Option - Description - Required - Default - - - - - - - `bucket` - - - - - A string indicating the bucket to upload files to. - - - - - Yes - - - - - \- - - - - - - - `spaces_url` - - - - - A string indicating either the Origin Endpoint or the CDN endpoint of your Spaces Object Storage bucket. - - - - - Yes - - - - - \- - - - - - - - `access_key_id` - - - - - A string indicating the [Spaces access key](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys). - - - - - Yes - - - - - \- - - - - - - - `secret_access_key` - - - - - A string indicating the [Spaces secret access key](https://docs.digitalocean.com/products/spaces/how-to/manage-access/#access-keys). - - - - - Yes - - - - - \- - - - - - - - `region` - - - - - A string indicating the region your Spaces Object Storage bucket is in. If you're unsure, you can find it in the Origin Endpoint whose format is `https://..digitaloceanspaces.com`. For example, `nyc3`. - - - - - Yes - - - - - \- - - - - - - - `endpoint` - - - - - A string indicating the Spaces Origin Endpoint. - - - - - Yes - - - - - \- - - - - - - - `download_url_duration` - - - - - A number indicating the expiry time of presigned URLs in seconds. - - - - - No - - - - - `60` - - - - -
- -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -SPACE_URL= -SPACE_BUCKET= -SPACE_REGION= -SPACE_ENDPOINT= -SPACE_ACCESS_KEY_ID= -SPACE_SECRET_ACCESS_KEY= -``` - ---- - -## Test the Plugin - -To test the plugin, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, upload a product image either using the [Medusa Admin](!user-guide!/products/manage#manage-product-media) or the [Admin API routes](https://docs.medusajs.com/api/admin#uploads_postuploads). - ---- - -## Next.js Starter Template Configuration - -If you’re using the [Next.js Starter storefront](../../../nextjs-starter/page.mdx), add the following option to the exported object in `next.config.js`: - -```jsx title="next.config.js" -const { withStoreConfig } = require("./store-config") - -// ... - -module.exports = withStoreConfig({ - // ... - images: { - domains: [ - // ... - "{spaces_domain}", - ], - }, -}) -``` - -This adds the Spaces domain into the configured images domain names. If you don't add the configuration, you’ll receive the error ["next/image Un-configured Host”](https://nextjs.org/docs/messages/next-image-unconfigured-host). - -Make sure to replace `{spaces_domain}` with the Spaces domain. It's of the format `..digitaloceanspaces.com` or `..cdn.digitaloceanspaces.com` diff --git a/www/apps/resources/app/commerce-modules/api-key/concepts/page.mdx b/www/apps/resources/app/commerce-modules/api-key/concepts/page.mdx index baaec0acc0..136f935f88 100644 --- a/www/apps/resources/app/commerce-modules/api-key/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/api-key/concepts/page.mdx @@ -12,7 +12,7 @@ There are two types of API keys: - `publishable`: A public key used in client applications, such as a storefront. - `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. -The API key’s type is stored in the `type` field of the [ApiKey data model](/references/api-key/models/ApiKey). +The API key’s type is stored in the `type` property of the [ApiKey data model](/references/api-key/models/ApiKey). --- diff --git a/www/apps/resources/app/commerce-modules/api-key/examples/page.mdx b/www/apps/resources/app/commerce-modules/api-key/examples/page.mdx index c6a576fa44..1fee0c10fc 100644 --- a/www/apps/resources/app/commerce-modules/api-key/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/api-key/examples/page.mdx @@ -25,7 +25,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu const apiKeyModuleService: IApiKeyModuleService = request.scope.resolve(ModuleRegistrationName.API_KEY) - const apiKey = await apiKeyModuleService.create({ + const apiKey = await apiKeyModuleService.createApiKeys({ title: "Publishable API key", type: "publishable", created_by: "user_123", @@ -50,7 +50,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu export async function POST(request: Request) { const apiKeyModuleService = await initializeApiKeyModule() - const apiKey = await apiKeyModuleService.create({ + const apiKey = await apiKeyModuleService.createApiKeys({ title: "Publishable API key", type: "publishable", created_by: "user_123", @@ -85,7 +85,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu request.scope.resolve(ModuleRegistrationName.API_KEY) res.json({ - api_keys: await apiKeyModuleService.list(), + api_keys: await apiKeyModuleService.listApiKeys(), }) } ``` @@ -104,7 +104,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu const apiKeyModuleService = await initializeApiKeyModule() return NextResponse.json({ - api_keys: await apiKeyModuleService.list(), + api_keys: await apiKeyModuleService.listApiKeys(), }) } ``` @@ -279,7 +279,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu } ) - const newKey = await apiKeyModuleService.create({ + const newKey = await apiKeyModuleService.createApiKeys({ title: revokedKey.title, type: revokedKey.type, created_by: revokedKey.created_by, @@ -318,7 +318,7 @@ In this guide, you’ll find common examples of how you can use the API Key Modu revoked_by: params.user_id, }) - const newKey = await apiKeyModuleService.create({ + const newKey = await apiKeyModuleService.createApiKeys({ title: revokedKey.title, type: revokedKey.type, created_by: revokedKey.created_by, diff --git a/www/apps/resources/app/commerce-modules/api-key/page.mdx b/www/apps/resources/app/commerce-modules/api-key/page.mdx index e2033a3101..b42f1e97c6 100644 --- a/www/apps/resources/app/commerce-modules/api-key/page.mdx +++ b/www/apps/resources/app/commerce-modules/api-key/page.mdx @@ -19,13 +19,13 @@ Store and manage API keys in your store. You can create both publishable and sec - Password reset tokens when a user or customer requests to reset their password. ```ts -const pubApiKey = await apiKeyModuleService.create({ +const pubApiKey = await apiKeyModuleService.createApiKeys({ title: "Publishable API key", type: "publishable", created_by: "user_123", }) -const secretApiKey = await apiKeyModuleService.create({ +const secretApiKey = await apiKeyModuleService.createApiKeys({ title: "Authentication Key", type: "secret", created_by: "user_123", @@ -66,7 +66,7 @@ const revokedKey = await apiKeyModuleService.revoke("apk_1", { revoked_by: "user_123", }) -const newKey = await apiKeyModuleService.create({ +const newKey = await apiKeyModuleService.createApiKeys({ title: revokedKey.title, type: revokedKey.type, created_by: revokedKey.created_by, @@ -97,7 +97,7 @@ For example: request.scope.resolve(ModuleRegistrationName.API_KEY) res.json({ - api_keys: await apiKeyModuleService.list(), + api_keys: await apiKeyModuleService.listApiKeys(), }) } ``` @@ -116,7 +116,7 @@ For example: const apiKeyModuleService: IApiKeyModuleService = container.resolve(ModuleRegistrationName.API_KEY) - const apiKeys = await apiKeyModuleService.list() + const apiKeys = await apiKeyModuleService.listApiKeys() } ``` @@ -136,7 +136,7 @@ For example: ModuleRegistrationName.API_KEY ) - const apiKeys = await apiKeyModuleService.list() + const apiKeys = await apiKeyModuleService.listApiKeys() }) ``` diff --git a/www/apps/resources/app/commerce-modules/auth/auth-flows/page.mdx b/www/apps/resources/app/commerce-modules/auth/auth-flows/page.mdx index 5eeed30e00..1b82797f17 100644 --- a/www/apps/resources/app/commerce-modules/auth/auth-flows/page.mdx +++ b/www/apps/resources/app/commerce-modules/auth/auth-flows/page.mdx @@ -45,7 +45,7 @@ Then, the user is authenticated successfully, and their authentication details a -Check out the [AuthIdentity](/references/auth/models/AuthIdentity) reference for the expected fields in `authIdentity`. +Check out the [AuthIdentity](/references/auth/models/AuthIdentity) reference for the expected properties in `authIdentity`. diff --git a/www/apps/resources/app/commerce-modules/auth/create-actor-type/page.mdx b/www/apps/resources/app/commerce-modules/auth/create-actor-type/page.mdx index 47ae4ef2c4..c71e393d9d 100644 --- a/www/apps/resources/app/commerce-modules/auth/create-actor-type/page.mdx +++ b/www/apps/resources/app/commerce-modules/auth/create-actor-type/page.mdx @@ -13,31 +13,18 @@ Before creating an actor type, you must define a data model the actor type belon The rest of this guide uses this `Manager` data model as an example: ```ts title="src/modules/manager/models/manager.ts" -import { BaseEntity } from "@medusajs/utils" -import { - Entity, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/utils" -@Entity() -export class Manager extends BaseEntity { - @PrimaryKey({ columnType: "text" }) - id!: string +const Manager = model.define("manager", { + id: model.id(), + firstName: model.text(), + lastName: model.text(), + email: model.text(), +}) - @Property({ columnType: "text" }) - first_name: string - - @Property({ columnType: "text" }) - last_name: string - - @Property({ columnType: "text" }) - email: string -} +export default Manager ``` -The module’s main service must also have a `create` method to create a record of the `Manager` data model. - --- ## 1. Create Workflow @@ -45,7 +32,7 @@ The module’s main service must also have a `create` method to create a record Start by creating a workflow that does two things: - Create a record of the `Manager` data model. -- Sets the `app_metadata` field of the associated `AuthIdentity` record based on the new actor type. +- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. For example, create the file `src/workflows/create-manager.ts`. with the following content: @@ -92,7 +79,7 @@ const createManagerStep = createStep( const managerModuleService: ManagerModuleService = container.resolve("managerModuleService") - const manager = await managerModuleService.create( + const manager = await managerModuleService.createManager( managerData ) @@ -127,7 +114,7 @@ This workflow accepts the manager’s data and the associated auth identity’s The workflow has two steps: 1. Create the manager using the `createManagerStep`. -2. Set the `app_metadata` field of the associated auth identity using the `setAuthAppMetadataStep` step imported from `@medusajs/core-flows`. You specify the actor type `manager` in the `actorType` property of the step’s input. +2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` step imported from `@medusajs/core-flows`. You specify the actor type `manager` in the `actorType` property of the step’s input. --- @@ -250,13 +237,12 @@ export async function GET( const managerModuleService: ManagerModuleService = req.scope.resolve("managerModuleService") - const manager = await managerModuleService.retrieve( + const manager = await managerModuleService.retrieveManager( req.auth_context.actor_id ) res.json({ manager }) } - ``` This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. diff --git a/www/apps/resources/app/commerce-modules/auth/examples/page.mdx b/www/apps/resources/app/commerce-modules/auth/examples/page.mdx index 3d27b69168..228743b5e2 100644 --- a/www/apps/resources/app/commerce-modules/auth/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/auth/examples/page.mdx @@ -235,11 +235,12 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j const authModuleService: IAuthModuleService = req.scope.resolve(ModuleRegistrationName.AUTH) - const authIdentity = await authModuleService.create({ - provider: "emailpass", - entity_id: "user@example.com", - scope: "admin", - }) + const authIdentity = await authModuleService + .createAuthIdentities({ + provider: "emailpass", + entity_id: "user@example.com", + scope: "admin", + }) res.json({ auth_identity: authIdentity }) } @@ -258,11 +259,12 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j export async function POST(request: Request) { const authModuleService = await initializeAuthModule() - const authIdentity = await authModuleService.create({ - provider: "emailpass", - entity_id: "user@example.com", - scope: "admin", - }) + const authIdentity = await authModuleService + .createAuthIdentities({ + provider: "emailpass", + entity_id: "user@example.com", + scope: "admin", + }) return NextResponse.json({ auth_identity: authIdentity, @@ -293,7 +295,8 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j req.scope.resolve(ModuleRegistrationName.AUTH) res.json({ - auth_identitys: await authModuleService.list(), + auth_identitys: + await authModuleService.listAuthIdentities(), }) } ``` @@ -312,7 +315,8 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j const authModuleService = await initializeAuthModule() return NextResponse.json({ - auth_identities: await authModuleService.list(), + auth_identities: + await authModuleService.listAuthIdentities(), }) } ``` @@ -339,12 +343,13 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j const authModuleService: IAuthModuleService = req.scope.resolve(ModuleRegistrationName.AUTH) - const authIdentity = await authModuleService.update({ - id: "authusr_123", - provider_metadata: { - test: true, - }, - }) + const authIdentity = await authModuleService + .updateAuthIdentites({ + id: "authusr_123", + provider_metadata: { + test: true, + }, + }) res.json({ auth_identity: authIdentity, @@ -374,15 +379,16 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j ) { const authModuleService = await initializeAuthModule() - const authIdentity = await authModuleService.update({ - id: "authusr_123", - provider_metadata: { - test: true, - }, - }) + const authIdentity = await authModuleService + .updateAuthIdentites({ + id: "authusr_123", + provider_metadata: { + test: true, + }, + }) return NextResponse.json({ - auth_identitys: await authModuleService.list(), + auth_identity: authIdentity, }) } ``` @@ -409,7 +415,9 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j const authModuleService: IAuthModuleService = req.scope.resolve(ModuleRegistrationName.AUTH) - await authModuleService.delete(["authusr_123"]) + await authModuleService.deleteAuthIdentities([ + "authusr_123", + ]) res.status(200) } @@ -437,7 +445,9 @@ This example uses the [jsonwebtoken NPM package](https://www.npmjs.com/package/j ) { const authModuleService = await initializeAuthModule() - await authModuleService.delete(["authusr_123"]) + await authModuleService.deleteAuthIdentities([ + "authusr_123", + ]) } ``` diff --git a/www/apps/resources/app/commerce-modules/auth/page.mdx b/www/apps/resources/app/commerce-modules/auth/page.mdx index 4656e1976e..d4163da844 100644 --- a/www/apps/resources/app/commerce-modules/auth/page.mdx +++ b/www/apps/resources/app/commerce-modules/auth/page.mdx @@ -99,7 +99,8 @@ For example: req.scope.resolve(ModuleRegistrationName.AUTH) res.json({ - authIdentitys: authModuleService.list(), + authIdentitys: + await authModuleService.listAuthIdentities(), }) } ``` @@ -120,7 +121,8 @@ For example: const authModuleService: IAuthModuleService = container.resolve(ModuleRegistrationName.AUTH) - const authIdentitys = await authModuleService.list() + const authIdentitys = await authModuleService + .listAuthIdentities() } ``` @@ -141,7 +143,8 @@ For example: container.resolve( ModuleRegistrationName.AUTH ) - const authIdentitys = await authModuleService.list() + const authIdentitys = await authModuleService + .listAuthIdentities() }) ``` diff --git a/www/apps/resources/app/commerce-modules/cart/concepts/page.mdx b/www/apps/resources/app/commerce-modules/cart/concepts/page.mdx index 82e9f9b7eb..e7d55c89d9 100644 --- a/www/apps/resources/app/commerce-modules/cart/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/cart/concepts/page.mdx @@ -18,7 +18,7 @@ A cart has a shipping and billing address. Both of these addresses are represent A line item, represented by the `LineItem` data model, is a product variant added to the cart. A cart has multiple line items. -A line item stores some of the product variant’s fields, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. +A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. @@ -36,8 +36,8 @@ If the shipping method is created from a shipping option, available through the A shipping method can also be a custom method associated with this cart only. -### data Field +### data Property -After an order is placed, you may use a third-party fulfillment provider to fulfill its shipments. If the fulfillment provider requires additional custom data to be passed along from the checkout process, you can add this data in the `ShippingMethod`'s `data` field. +After an order is placed, you may use a third-party fulfillment provider to fulfill its shipments. If the fulfillment provider requires additional custom data to be passed along from the checkout process, you can add this data in the `ShippingMethod`'s `data` property. -The `data` field is an object used to store custom data relevant later for fulfillment. +The `data` property is an object used to store custom data relevant later for fulfillment. diff --git a/www/apps/resources/app/commerce-modules/cart/examples/page.mdx b/www/apps/resources/app/commerce-modules/cart/examples/page.mdx index b79af7187d..84caf5dd66 100644 --- a/www/apps/resources/app/commerce-modules/cart/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/cart/examples/page.mdx @@ -25,7 +25,7 @@ In this guide, you’ll find common examples of how you can use the Cart Module const cartModuleService: ICartModuleService = req.scope.resolve(ModuleRegistrationName.CART) - const cart = await cartModuleService.create({ + const cart = await cartModuleService.createCarts({ currency_code: "usd", shipping_address: { address_1: "1512 Barataria Blvd", @@ -57,7 +57,7 @@ In this guide, you’ll find common examples of how you can use the Cart Module export async function POST(request: Request) { const cartModuleService = await initializeCartModule() - const cart = await cartModuleService.create({ + const cart = await cartModuleService.createCarts({ currency_code: "usd", shipping_address: { address_1: "1512 Barataria Blvd", @@ -100,7 +100,9 @@ In this guide, you’ll find common examples of how you can use the Cart Module const cartModuleService: ICartModuleService = req.scope.resolve(ModuleRegistrationName.CART) - const cart = await cartModuleService.retrieve("cart_123") + const cart = await cartModuleService.retrieveCart( + "cart_123" + ) res.json({ cart }) } @@ -119,7 +121,9 @@ In this guide, you’ll find common examples of how you can use the Cart Module export async function GET(request: Request) { const cartModuleService = await initializeCartModule() - const cart = await cartModuleService.retrieve("cart_123") + const cart = await cartModuleService.retrieveCart( + "cart_123" + ) return NextResponse.json({ cart, diff --git a/www/apps/resources/app/commerce-modules/cart/page.mdx b/www/apps/resources/app/commerce-modules/cart/page.mdx index 2828124707..5e1d321af9 100644 --- a/www/apps/resources/app/commerce-modules/cart/page.mdx +++ b/www/apps/resources/app/commerce-modules/cart/page.mdx @@ -15,7 +15,7 @@ The Cart Module is the `@medusajs/cart` NPM package that provides cart-related f Store and manage carts, including their addresses, line items, shipping methods, and more. ```ts -const cart = await cartModuleService.create({ +const cart = await cartModuleService.createCarts({ currency_code: "usd", shipping_address: { address_1: "1512 Barataria Blvd", @@ -76,7 +76,6 @@ For example: import { MedusaRequest, MedusaResponse } from "@medusajs/medusa" import { ICartModuleService } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/modules-sdk" - import { res } from "express" export async function GET( req: MedusaRequest, @@ -86,7 +85,7 @@ For example: req.scope.resolve(ModuleRegistrationName.CART) res.json({ - carts: await cartModuleService.list(), + carts: await cartModuleService.listCarts(), }) } ``` @@ -105,7 +104,7 @@ For example: const cartModuleService: ICartModuleService = container.resolve(ModuleRegistrationName.CART) - const carts = await cartModuleService.list() + const carts = await cartModuleService.listCarts() } ``` @@ -125,7 +124,7 @@ For example: ModuleRegistrationName.CART ) - const carts = await cartModuleService.list() + const carts = await cartModuleService.listCarts() }) ``` diff --git a/www/apps/resources/app/commerce-modules/cart/promotions/page.mdx b/www/apps/resources/app/commerce-modules/cart/promotions/page.mdx index 7d25ac750c..3e8228f0bc 100644 --- a/www/apps/resources/app/commerce-modules/cart/promotions/page.mdx +++ b/www/apps/resources/app/commerce-modules/cart/promotions/page.mdx @@ -18,13 +18,13 @@ The `LineItemAdjustment` data model represents changes on a line item, and the ` ![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) -The `amount` field of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` field of the adjustment line. +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. --- ## Discountable Option -The `LineItem` data model has an `is_discountable` field that indicates whether promotions can be applied to the line item. It’s enabled by default. +The `LineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. @@ -50,7 +50,7 @@ import { } from "@medusajs/types" // retrieve the cart -const cart = await cartModuleService.retrieve("cart_123", { +const cart = await cartModuleService.retrieveCart("cart_123", { relations: [ "items.adjustments", "shipping_methods.adjustments", diff --git a/www/apps/resources/app/commerce-modules/cart/tax-lines/page.mdx b/www/apps/resources/app/commerce-modules/cart/tax-lines/page.mdx index 6f2466e5a4..40bed45a41 100644 --- a/www/apps/resources/app/commerce-modules/cart/tax-lines/page.mdx +++ b/www/apps/resources/app/commerce-modules/cart/tax-lines/page.mdx @@ -18,7 +18,7 @@ A tax line indicates the tax rate of a line item or a shipping method. The [Line By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then added to the item/method’s subtotal. -However, line items and shipping methods have an `is_tax_inclusive` field that, when enabled, indicates that the item or method’s price already includes taxes. +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. @@ -38,7 +38,7 @@ When using the Cart and Tax modules together, you can use the `getTaxLines` meth ```ts // retrieve the cart -const cart = await cartModuleService.retrieve("cart_123", { +const cart = await cartModuleService.retrieveCart("cart_123", { relations: [ "items.tax_lines", "shipping_methods.tax_lines", diff --git a/www/apps/resources/app/commerce-modules/currency/examples/page.mdx b/www/apps/resources/app/commerce-modules/currency/examples/page.mdx index 36a1667f0a..16c9ffe303 100644 --- a/www/apps/resources/app/commerce-modules/currency/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/currency/examples/page.mdx @@ -26,7 +26,7 @@ In this guide, you’ll find common examples of how you can use the Currency Mod req.scope.resolve(ModuleRegistrationName.CURRENCY) res.json({ - currencies: await currencyModuleService.list(), + currencies: await currencyModuleService.listCurrencies(), }) } ``` @@ -42,10 +42,11 @@ In this guide, you’ll find common examples of how you can use the Currency Mod } from "@medusajs/currency" export async function GET(request: Request) { - const currencyModuleService = await initializeCurrencyModule() + const currencyModuleService = + await initializeCurrencyModule() return NextResponse.json({ - currencies: await currencyModuleService.list(), + currencies: await currencyModuleService.listCurrencies(), }) } ``` @@ -72,7 +73,8 @@ In this guide, you’ll find common examples of how you can use the Currency Mod const currencyModuleService: ICurrencyModuleService = req.scope.resolve(ModuleRegistrationName.CURRENCY) - const currency = await currencyModuleService.retrieve("usd") + const currency = await currencyModuleService + .retrieveCurrency("usd") res.json({ currency, @@ -95,7 +97,8 @@ In this guide, you’ll find common examples of how you can use the Currency Mod ) { const currencyModuleService = await initializeCurrencyModule() - const currency = await currencyModuleService.retrieve("usd") + const currency = await currencyModuleService + .retrieveCurrency("usd") return NextResponse.json({ currency }) } diff --git a/www/apps/resources/app/commerce-modules/currency/page.mdx b/www/apps/resources/app/commerce-modules/currency/page.mdx index f587673a63..02660d9d30 100644 --- a/www/apps/resources/app/commerce-modules/currency/page.mdx +++ b/www/apps/resources/app/commerce-modules/currency/page.mdx @@ -15,7 +15,9 @@ The Currency Module is the `@medusajs/currency` NPM package that provides curren List and retrieve currencies stored in your application. ```ts -const currency = await currencyModuleService.retrieve("usd") +const currency = await currencyModuleService.retrieveCurrency( + "usd" +) ``` ### Support Currencies in Modules @@ -25,8 +27,10 @@ Other commerce modules use currency codes in their data models or operations. Yo An example with the Region Module: ```ts -const region = await regionModuleService.retrieve("reg_123") -const currency = await currencyModuleService.retrieve( +const region = await regionModuleService.retrieveCurrency( + "reg_123" +) +const currency = await currencyModuleService.retrieveCurrency( region.currency_code ) ``` @@ -55,7 +59,7 @@ For example: req.scope.resolve(ModuleRegistrationName.CURRENCY) res.json({ - currencies: await currencyModuleService.list(), + currencies: await currencyModuleService.listCurrencies(), }) } ``` @@ -74,7 +78,8 @@ For example: const currencyModuleService: ICurrencyModuleService = container.resolve(ModuleRegistrationName.CURRENCY) - const currencies = await currencyModuleService.list() + const currencies = await currencyModuleService + .listCurrencies() } ``` @@ -94,7 +99,8 @@ For example: ModuleRegistrationName.CURRENCY ) - const currencies = await currencyModuleService.list() + const currencies = await currencyModuleService + .listCurrencies() }) ``` diff --git a/www/apps/resources/app/commerce-modules/customer/customer-accounts/page.mdx b/www/apps/resources/app/commerce-modules/customer/customer-accounts/page.mdx index b5b38fb4c8..54dda6c6e5 100644 --- a/www/apps/resources/app/commerce-modules/customer/customer-accounts/page.mdx +++ b/www/apps/resources/app/commerce-modules/customer/customer-accounts/page.mdx @@ -6,9 +6,9 @@ export const metadata = { In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. -## `has_account` field +## `has_account` Property -The [Customer data model](/references/customer/models/Customer) has a `has_account` field, which is a boolean that indicates whether a customer is registerd. +The [Customer data model](/references/customer/models/Customer) has a `has_account` property, which is a boolean that indicates whether a customer is registerd. When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. @@ -18,6 +18,6 @@ When this or another guest customer registers an account with the same email, a ## Email Uniqueness -The above behavior means that two `Customer` records may exist of the same email. However, the main difference is the `has_account` field’s value. +The above behavior means that two `Customer` records may exist of the same email. However, the main difference is the `has_account` property's value. So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. diff --git a/www/apps/resources/app/commerce-modules/customer/examples/page.mdx b/www/apps/resources/app/commerce-modules/customer/examples/page.mdx index 0df8eeb2a8..7a127e398a 100644 --- a/www/apps/resources/app/commerce-modules/customer/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/customer/examples/page.mdx @@ -25,7 +25,7 @@ In this guide, you’ll find common examples of how you can use the Customer Mod const customerModuleService: ICustomerModuleService = request.scope.resolve(ModuleRegistrationName.CUSTOMER) - const customer = await customerModuleService.create({ + const customer = await customerModuleService.createCustomers({ first_name: "Peter", last_name: "Hayes", email: "peter.hayes@example.com", @@ -50,7 +50,7 @@ In this guide, you’ll find common examples of how you can use the Customer Mod export async function POST(request: Request) { const customerModuleService = await initializeCustomerModule() - const customer = await customerModuleService.create({ + const customer = await customerModuleService.createCustomers({ first_name: "Peter", last_name: "Hayes", email: "peter.hayes@example.com", @@ -85,7 +85,7 @@ In this guide, you’ll find common examples of how you can use the Customer Mod request.scope.resolve(ModuleRegistrationName.CUSTOMER) const customerGroup = - await customerModuleService.createCustomerGroup({ + await customerModuleService.createCustomerGroups({ name: "VIP", }) @@ -109,7 +109,7 @@ In this guide, you’ll find common examples of how you can use the Customer Mod const customerModuleService = await initializeCustomerModule() const customerGroup = - await customerModuleService.createCustomerGroup({ + await customerModuleService.createCustomerGroups({ name: "VIP", }) diff --git a/www/apps/resources/app/commerce-modules/customer/page.mdx b/www/apps/resources/app/commerce-modules/customer/page.mdx index 09c72ca297..73979c8cb7 100644 --- a/www/apps/resources/app/commerce-modules/customer/page.mdx +++ b/www/apps/resources/app/commerce-modules/customer/page.mdx @@ -15,7 +15,7 @@ The Customer Module is the `@medusajs/customer` NPM package that provides custom With the Customer Module, store and manage customers in your store. ```ts -const customer = await customerModuleService.create({ +const customer = await customerModuleService.createCustomers({ first_name: "Peter", last_name: "Hayes", email: "peter.hayes@example.com", @@ -28,7 +28,7 @@ You can organize customers into groups. This has a lot of benefits and supports ```ts const customerGroup = - await customerModuleService.createCustomerGroup({ + await customerModuleService.createCustomerGroups({ name: "VIP", }) @@ -62,7 +62,7 @@ For example: request.scope.resolve(ModuleRegistrationName.CUSTOMER) res.json({ - customers: await customerModuleService.list(), + customers: await customerModuleService.listCustomers(), }) } ``` @@ -81,7 +81,7 @@ For example: const customerModuleService: ICustomerModuleService = container.resolve(ModuleRegistrationName.CUSTOMER) - const customers = await customerModuleService.list() + const customers = await customerModuleService.listCustomers() } ``` @@ -99,7 +99,7 @@ For example: const customerModuleService: ICustomerModuleService = container.resolve(ModuleRegistrationName.CUSTOMER) - const customers = await customerModuleService.list() + const customers = await customerModuleService.listCustomers() }) ``` diff --git a/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/page.mdx b/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/page.mdx index 2202563a76..645e01a44a 100644 --- a/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/page.mdx +++ b/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/page.mdx @@ -22,11 +22,11 @@ The fulfillment is also associated with a shipping option of that provider, whic --- -## data Field +## data Property -The `Fulfillment` data model has a `data` field that holds any necessary data for the third-party fulfillment provider to process the fulfillment. +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. -For example, the `data` field can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. +For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. --- @@ -48,7 +48,7 @@ Once a shipment is created for the fulfillment, you can store its tracking numbe ## Fulfillment Status -The `Fulfillment` data model has three fields to keep track of the current status of the fulfillment: +The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: - `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. - `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. diff --git a/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/page.mdx b/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/page.mdx index d4c52561a8..6ecfcd319b 100644 --- a/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/page.mdx +++ b/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/page.mdx @@ -34,9 +34,9 @@ Service zones can be more restrictive, such as restricting to certain cities or You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. -These rules are represented by the [ShippingOptionRule data model](/references/fulfillment/models/ShippingOptionRule). Its fields define the custom rule: +These rules are represented by the [ShippingOptionRule data model](/references/fulfillment/models/ShippingOptionRule). Its properties define the custom rule: -- `attribute`: The name of a field or table that the rule applies to. For example, `customer_group`. +- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. - `operator`: The operator used in the condition. For example: - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. @@ -59,8 +59,8 @@ A shipping option also belongs to a shipping profile, as each shipping profile d --- -## data Field +## data Property When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. -The `ShippingOption` data model has a `data` field. It's an object that stores custom data relevant later when creating and processing a fulfillment. +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. diff --git a/www/apps/resources/app/commerce-modules/inventory/concepts/page.mdx b/www/apps/resources/app/commerce-modules/inventory/concepts/page.mdx index f2da158810..48b2dbb028 100644 --- a/www/apps/resources/app/commerce-modules/inventory/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/inventory/concepts/page.mdx @@ -20,15 +20,15 @@ The `InventoryItem` data model mainly holds details related to the underlying st An inventory level, represented by the [InventoryLevel data model](/references/inventory/models/InventoryLevel), holds the inventory and quantity details of an inventory item in a specific location. -It has three quantity-related fields: +It has three quantity-related properties: - `stocked_quantity`: The available stock quantity of an item in the associated location. - `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. -- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This field doesn't play into the `stocked_quantity` or when checking whether an item is in stock. +- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. ### Associated Location -The inventory level's location is determined by the `location_id` field. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. +The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. --- diff --git a/www/apps/resources/app/commerce-modules/inventory/examples/page.mdx b/www/apps/resources/app/commerce-modules/inventory/examples/page.mdx index f4dd1dab13..f6347befd0 100644 --- a/www/apps/resources/app/commerce-modules/inventory/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/inventory/examples/page.mdx @@ -26,7 +26,7 @@ In this document, you’ll find common examples of how you can use the Inventory request.scope.resolve(ModuleRegistrationName.INVENTORY) const inventoryItem = - await inventoryModuleService.create({ + await inventoryModuleService.createInventoryItems({ sku: request.body.sku, title: request.body.title, requires_shipping: request.body.requires_shipping, @@ -52,7 +52,7 @@ In this document, you’ll find common examples of how you can use the Inventory const body = await request.json() const inventoryItem = - await inventoryModuleService.create({ + await inventoryModuleService.createInventoryItems({ sku: body.sku, title: body.title, requires_shipping: body.requires_shipping, @@ -85,7 +85,7 @@ In this document, you’ll find common examples of how you can use the Inventory request.scope.resolve(ModuleRegistrationName.INVENTORY) const inventoryItems = - await inventoryModuleService.list({}) + await inventoryModuleService.listInventoryItems({}) res.json({ inventory_items: inventoryItems }) } @@ -106,7 +106,7 @@ In this document, you’ll find common examples of how you can use the Inventory await initializeInventoryModule({}) const inventoryItems = - await inventoryModuleService.list({}) + await inventoryModuleService.listInventoryItems({}) return NextResponse.json({ inventory_items: inventoryItems }) } @@ -135,7 +135,7 @@ In this document, you’ll find common examples of how you can use the Inventory request.scope.resolve(ModuleRegistrationName.INVENTORY) const inventoryItem = - await inventoryModuleService.retrieve( + await inventoryModuleService.retrieveInventoryItem( request.params.id ) @@ -167,7 +167,7 @@ In this document, you’ll find common examples of how you can use the Inventory await initializeInventoryModule({}) const inventoryItem = - await inventoryModuleService.retrieve( + await inventoryModuleService.retrieveInventoryItem( params.id ) diff --git a/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/page.mdx b/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/page.mdx index 7fd560604c..4630fe05be 100644 --- a/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/page.mdx +++ b/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/page.mdx @@ -8,7 +8,7 @@ This document explains how the Inventory Module is used within the Medusa applic ## Product Variant Creation -When a product variant is created and its `manage_inventory` field's value is `true`, the Medusa application creates an inventory item associated with that product variant. +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. ![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) @@ -32,7 +32,7 @@ When an order is placed, the Medusa application creates a reservation item for e ## Order Fulfillment -When an item in an order is fulfilled and the associated variant has its `manage_inventory` field set to `true`, the Medusa application: +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: - Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. - Resets the `reserved_quantity` to `0`. @@ -44,6 +44,6 @@ When an item in an order is fulfilled and the associated variant has its `manage ## Order Return -When an item in an order is returned and the associated variant has its `manage_inventory` field set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. ![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) diff --git a/www/apps/resources/app/commerce-modules/inventory/page.mdx b/www/apps/resources/app/commerce-modules/inventory/page.mdx index 3753a64e92..c7df55f915 100644 --- a/www/apps/resources/app/commerce-modules/inventory/page.mdx +++ b/www/apps/resources/app/commerce-modules/inventory/page.mdx @@ -18,7 +18,7 @@ Inventory items hold details of the underlying stock-kept item, as well as inven ```ts const inventoryItem = - await inventoryModuleService.create({ + await inventoryModuleService.createInventoryItems({ sku: "SHIRT", title: "Green Medusa Shirt", requires_shipping: true, @@ -89,7 +89,7 @@ For example: res.json({ inventory_items: - await inventoryModuleService.list({}), + await inventoryModuleService.listInventoryItems({}), }) } ``` @@ -109,7 +109,7 @@ For example: container.resolve(ModuleRegistrationName.INVENTORY) const inventoryItems = - await inventoryModuleService.list({}) + await inventoryModuleService.listInventoryItems({}) } ``` @@ -128,7 +128,7 @@ For example: container.resolve(ModuleRegistrationName.INVENTORY) const inventoryItems = - await inventoryModuleService.list({}) + await inventoryModuleService.listInventoryItems({}) }) ``` diff --git a/www/apps/resources/app/commerce-modules/inventory/relations-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/inventory/relations-to-other-modules/page.mdx index dbfe32e780..ceaeddae9a 100644 --- a/www/apps/resources/app/commerce-modules/inventory/relations-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/inventory/relations-to-other-modules/page.mdx @@ -12,7 +12,7 @@ Each product variant has different inventory details. Medusa defines a link modu ![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) -A product variant, whose `manage_inventory` field is enabled, has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. +A product variant, whose `manage_inventory` property is enabled, has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. --- diff --git a/www/apps/resources/app/commerce-modules/order/concepts/page.mdx b/www/apps/resources/app/commerce-modules/order/concepts/page.mdx index 63bf2c6e5d..7c01a388a0 100644 --- a/www/apps/resources/app/commerce-modules/order/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/order/concepts/page.mdx @@ -22,13 +22,13 @@ The details of the purchased products are represented by the [LineItem data mode An order has one or more shipping methods used to handle item shipment. Each shipping method is represented by the [ShippingMethod data model](/references/order/models/ShippingMethod) that holds its details. -### data Field +### data Property When fulfilling the order, you may use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. -The `ShippingMethod` data model has a `data` field. It’s an object used to store custom data relevant later for fulfillment. +The `ShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. -The Medusa application passes the `data` field to the Fulfillment Module when fulfilling items. +The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. --- diff --git a/www/apps/resources/app/commerce-modules/order/order-change/page.mdx b/www/apps/resources/app/commerce-modules/order/order-change/page.mdx index f6a4818077..12982241b0 100644 --- a/www/apps/resources/app/commerce-modules/order/order-change/page.mdx +++ b/www/apps/resources/app/commerce-modules/order/order-change/page.mdx @@ -35,7 +35,7 @@ Each of these actions is represented by the [OrderChangeAction data model](/refe ### Action Name -The `action` field of the `OrderChangeAction` holds the name of the action to perform. Based on the action, additional details are stored in the `details` object field of the data model. +The `action` property of the `OrderChangeAction` holds the name of the action to perform. Based on the action, additional details are stored in the `details` object property of the data model. @@ -162,7 +162,7 @@ The `action` field of the `OrderChangeAction` holds the name of the action to pe Mark as received an item whose return was previously requested, but consider the items damaged. - This changes the order item’s `return_dismissed_quantity` field rather than its `return_received_quantity` field. + This changes the order item’s `return_dismissed_quantity` property rather than its `return_received_quantity` property. @@ -223,7 +223,7 @@ The `action` field of the `OrderChangeAction` holds the name of the action to pe - Mark a quantity of an item as shipped. This modifies the shipped_quantity field of an order item. + Mark a quantity of an item as shipped. This modifies the `shipped_quantity` property of an order item. @@ -243,7 +243,7 @@ The `action` field of the `OrderChangeAction` holds the name of the action to pe - Mark a quantity of an item as fulfilled. This modifies the fulfilled_quantity field of an order item. + Mark a quantity of an item as fulfilled. This modifies the `fulfilled_quantity` property of an order item. @@ -277,7 +277,7 @@ The `action` field of the `OrderChangeAction` holds the name of the action to pe ## Order Change Confirmation -The `OrderChange` data model has a `status` field that indicates its current status. By default, it’s pending. At this point, the order change’s actions aren’t applied to the order yet. +The `OrderChange` data model has a `status` property that indicates its current status. By default, it’s pending. At this point, the order change’s actions aren’t applied to the order yet. To apply these changes to the order, you confirm the order change. When the order change is confirmed: diff --git a/www/apps/resources/app/commerce-modules/order/order-versioning/page.mdx b/www/apps/resources/app/commerce-modules/order/order-versioning/page.mdx index bb859a4c94..9ab2a49786 100644 --- a/www/apps/resources/app/commerce-modules/order/order-versioning/page.mdx +++ b/www/apps/resources/app/commerce-modules/order/order-versioning/page.mdx @@ -12,17 +12,17 @@ Versioning means assigning a version number to a record, such as an order and it --- -## version Field +## version Property -The `Order` and `OrderSummary` data models have a `version` field that indicates the current order version. By default, its value is `1`. +The `Order` and `OrderSummary` data models have a `version` property that indicates the current order version. By default, its value is `1`. -The `OrderItem` data model also has a `version` field, but it indicates the version it belongs to. For example, original items in the order have version `1`. Then, when a new item is added, its version is `2`. If an existing item is modified, such as its quantity has been changed, its version is updated. +The `OrderItem` data model also has a `version` property, but it indicates the version it belongs to. For example, original items in the order have version `1`. Then, when a new item is added, its version is `2`. If an existing item is modified, such as its quantity has been changed, its version is updated. --- ## Order Change Versioning -The `OrderChange` and `OrderChangeAction` data models also have a `version` field. When an order change is created, the `version` field’s value is the associated order’s version incremented. +The `OrderChange` and `OrderChangeAction` data models also have a `version` property. When an order change is created, the `version` property’s value is the associated order’s version incremented. So, if the order’s `version` is `1`, the order change’s version is `2`. diff --git a/www/apps/resources/app/commerce-modules/order/page.mdx b/www/apps/resources/app/commerce-modules/order/page.mdx index aa18e06e92..827bb88a1c 100644 --- a/www/apps/resources/app/commerce-modules/order/page.mdx +++ b/www/apps/resources/app/commerce-modules/order/page.mdx @@ -15,7 +15,7 @@ The Order Module is the `@medusajs/order` NPM package that provides order-relate Store and manage your orders to retriev, create, cancel, and perform other operations. ```ts -const order = await orderModuleService.create({ +const order = await orderModuleService.createOrders({ currency_code: "usd", items: [ { @@ -38,7 +38,7 @@ const order = await orderModuleService.create({ Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. ```ts -const draftOrder = await orderModuleService.create({ +const draftOrder = await orderModuleService.createOrders({ currency_code: "usd", // other details... status: "draft", @@ -122,7 +122,7 @@ For example: req.scope.resolve(ModuleRegistrationName.ORDER) res.json({ - orders: await orderModuleService.list(), + orders: await orderModuleService.listOrders(), }) } ``` @@ -141,7 +141,7 @@ For example: const orderModuleService: IOrderModuleService = container.resolve(ModuleRegistrationName.ORDER) - const orders = await orderModuleService.list() + const orders = await orderModuleService.listOrders() } ``` @@ -160,7 +160,7 @@ For example: container.resolve( ModuleRegistrationName.ORDER ) - const orders = await orderModuleService.list() + const orders = await orderModuleService.listOrders() }) ``` diff --git a/www/apps/resources/app/commerce-modules/order/promotion-adjustments/page.mdx b/www/apps/resources/app/commerce-modules/order/promotion-adjustments/page.mdx index 6bfe6500de..c8948e4e09 100644 --- a/www/apps/resources/app/commerce-modules/order/promotion-adjustments/page.mdx +++ b/www/apps/resources/app/commerce-modules/order/promotion-adjustments/page.mdx @@ -18,13 +18,13 @@ The [LineItemAdjustment data model](/references/order/models/LineItemAdjustment) ![A diagram showcasing the relation between an order, its items and shipping methods, and their adjustment lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712306017/Medusa%20Resources/order-adjustments_myflir.jpg) -The `amount` field of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion can be stored in the `promotion_id` field of the adjustment line. +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion can be stored in the `promotion_id` property of the adjustment line. --- ## Discountable Option -The `LineItem` data model has an `is_discountable` field that indicates whether promotions can be applied to the line item. It’s enabled by default. +The `LineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. @@ -52,7 +52,7 @@ import { // ... // retrieve the order -const order = await orderModuleService.retrieve("ord_123", { +const order = await orderModuleService.retrieveOrder("ord_123", { relations: [ "items.item.adjustments", "shipping_methods.adjustments", diff --git a/www/apps/resources/app/commerce-modules/order/tax-lines/page.mdx b/www/apps/resources/app/commerce-modules/order/tax-lines/page.mdx index 8435bced2b..b3b49240f6 100644 --- a/www/apps/resources/app/commerce-modules/order/tax-lines/page.mdx +++ b/www/apps/resources/app/commerce-modules/order/tax-lines/page.mdx @@ -22,7 +22,7 @@ A tax line indicates the tax rate of a line item or a shipping method. The [Line By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. -However, line items and shipping methods have an `is_tax_inclusive` field that, when enabled, indicates that the item or method’s price already includes taxes. +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. diff --git a/www/apps/resources/app/commerce-modules/order/transactions/page.mdx b/www/apps/resources/app/commerce-modules/order/transactions/page.mdx index 1973383c63..5e01aa1bde 100644 --- a/www/apps/resources/app/commerce-modules/order/transactions/page.mdx +++ b/www/apps/resources/app/commerce-modules/order/transactions/page.mdx @@ -18,7 +18,7 @@ The transaction’s main purpose is to ensure a correct balance between paid and ## Checking Outstanding Amount -The order’s total is stored in the `OrderSummary`'s `total` field. To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: +The order’s total is stored in the `OrderSummary`'s `total` property. To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked:
@@ -73,7 +73,7 @@ The order’s total is stored in the `OrderSummary`'s `total` field. To check th The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. For that, use the Payment Module or custom logic. -The `Transaction` data model has two fields that determine which data model and record holds the actual payment’s details: +The `Transaction` data model has two properties that determine which data model and record holds the actual payment’s details: - `reference`: indicates the table’s name in the database. For example, `payment` if you’re using the Payment Module. - `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. \ No newline at end of file diff --git a/www/apps/resources/app/commerce-modules/payment/payment-session/page.mdx b/www/apps/resources/app/commerce-modules/payment/payment-session/page.mdx index 1f0b04dd28..76d911a357 100644 --- a/www/apps/resources/app/commerce-modules/payment/payment-session/page.mdx +++ b/www/apps/resources/app/commerce-modules/payment/payment-session/page.mdx @@ -16,17 +16,17 @@ A payment collection can have multiple payment sessions. For example, during che --- -## data field +## data Property -Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` field used to store that data. +Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. -For example, the customer's ID in Stripe is stored in the `data` field. +For example, the customer's ID in Stripe is stored in the `data` property. --- ## Payment Session Status -The `status` field of a payment session indicates its current status. Its value can be: +The `status` property of a payment session indicates its current status. Its value can be: - `pending`: The payment session is awaiting authorization. - `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. diff --git a/www/apps/resources/app/commerce-modules/payment/payment/page.mdx b/www/apps/resources/app/commerce-modules/payment/payment/page.mdx index 2cbeef6fc0..46458c5623 100644 --- a/www/apps/resources/app/commerce-modules/payment/payment/page.mdx +++ b/www/apps/resources/app/commerce-modules/payment/payment/page.mdx @@ -14,7 +14,7 @@ A payment carries along many of the data and relations of a payment session: - It belongs to the same payment collection. - It’s associated with the same payment provider, which handles further payment processing. -- It stores the payment session’s `data` field in its `data` field, as it’s still useful for the payment provider’s processing. +- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing. --- diff --git a/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx b/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx index 233c8d4bc4..b40e38ff06 100644 --- a/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx @@ -18,7 +18,7 @@ A [PriceSet](/references/pricing/models/PriceSet) represents a collection of pri Each price within a price set can be applied for different conditions. These conditions are represented as rule types. -A [RuleType](/references/pricing/models/RuleType) defines custom conditions. A rule type has a unique `rule_attribute` which indicates the field this rule applies on. For example, `region_id`. +A [RuleType](/references/pricing/models/RuleType) defines custom conditions. A rule type has a unique `rule_attribute` which indicates the property this rule applies on. For example, `region_id`. This is referenced when setting a rule of a price. For example: @@ -46,6 +46,6 @@ const priceSet = await pricingModuleService.addPrices({ ## Price List -A [PriceList](/references/pricing/models/PriceList) is a group of prices only enabled if their conditions and rules are satisfied. A price list has optional `start_date` and `end_date` fields, which indicate the date range in which a price list can be applied. +A [PriceList](/references/pricing/models/PriceList) is a group of prices only enabled if their conditions and rules are satisfied. A price list has optional `start_date` and `end_date` properties, which indicate the date range in which a price list can be applied. Its associated prices are represented by the `Price` data model. \ No newline at end of file diff --git a/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx b/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx index a6f14f5c43..3871402efa 100644 --- a/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/examples/page.mdx @@ -27,7 +27,7 @@ In this document, you’ll find common examples of how you can use the Pricing M // A rule type with \`rule_field=region_id\` should // already be present in the database - const priceSet = await pricingModuleService.create([ + const priceSet = await pricingModuleService.createPriceSets([ { rules: [{ rule_field: "region_id" }], prices: [ @@ -62,7 +62,7 @@ In this document, you’ll find common examples of how you can use the Pricing M // A rule type with \`rule_field=region_i\` should // already be present in the database - const priceSet = await pricingModuleService.create([ + const priceSet = await pricingModuleService.createPriceSets([ { rules: [{ rule_field: "region_id" }], prices: [ @@ -103,7 +103,7 @@ In this document, you’ll find common examples of how you can use the Pricing M const pricingModuleService: IPricingModuleService = request.scope.resolve(ModuleRegistrationName.PRICING) - const priceSets = await pricingModuleService.list() + const priceSets = await pricingModuleService.listPriceSets() res.json({ price_sets: priceSets }) } @@ -122,7 +122,7 @@ In this document, you’ll find common examples of how you can use the Pricing M export async function GET(request: Request) { const pricingModuleService = await initializePricingModule() - const priceSets = await pricingModuleService.list() + const priceSets = await pricingModuleService.listPriceSets() return NextResponse.json({ price_sets: priceSets }) } @@ -150,7 +150,7 @@ In this document, you’ll find common examples of how you can use the Pricing M const pricingModuleService: IPricingModuleService = request.scope.resolve(ModuleRegistrationName.PRICING) - const priceSet = await pricingModuleService.retrieve( + const priceSet = await pricingModuleService.retrievePriceSet( request.params.id ) @@ -180,7 +180,7 @@ In this document, you’ll find common examples of how you can use the Pricing M ) { const pricingModuleService = await initializePricingModule() - const priceSet = await pricingModuleService.retrieve( + const priceSet = await pricingModuleService.retrievePriceSet( params.id ) diff --git a/www/apps/resources/app/commerce-modules/pricing/page.mdx b/www/apps/resources/app/commerce-modules/pricing/page.mdx index 11be06808f..ba5b800d93 100644 --- a/www/apps/resources/app/commerce-modules/pricing/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/page.mdx @@ -17,7 +17,7 @@ With the Pricing Module, store the prices of a resource and manage them through Prices are grouped in a price set, allowing you to add more than one price for a resource based on different conditions, such as currency code. ```ts -const priceSet = await pricingModuleService.create({ +const priceSet = await pricingModuleService.createPriceSets({ rules: [], prices: [ { @@ -124,7 +124,7 @@ For example: request.scope.resolve(ModuleRegistrationName.PRICING) res.json({ - price_sets: await pricingModuleService.list(), + price_sets: await pricingModuleService.listPriceSets(), }) } ``` @@ -143,7 +143,7 @@ For example: const pricingModuleService: IPricingModuleService = container.resolve(ModuleRegistrationName.PRICING) - const priceSets = await pricingModuleService.list() + const priceSets = await pricingModuleService.listPriceSets() } ``` @@ -161,7 +161,7 @@ For example: const pricingModuleService: IPricingModuleService = container.resolve(ModuleRegistrationName.PRICING) - const priceSets = await pricingModuleService.list() + const priceSets = await pricingModuleService.listPriceSets() }) ``` diff --git a/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx b/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx index 6341b5d080..bb3249a9c1 100644 --- a/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx @@ -178,7 +178,7 @@ const ruleTypes = await pricingModuleService.createRuleTypes([ }, ]) -const priceSet = await pricingModuleService.create({ +const priceSet = await pricingModuleService.createPriceSets({ rules: [ { rule_attribute: "region_id" }, { rule_attribute: "city" }, @@ -422,7 +422,7 @@ const priceSet = await pricingModuleService.create({ ```ts - const priceList = pricingModuleService.createPriceList({ + const priceList = pricingModuleService.createPriceLists({ name: "Test Price List", starts_at: Date.parse("01/10/2023"), ends_at: Date.parse("31/10/2023"), diff --git a/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx b/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx index b73308b584..9108687baf 100644 --- a/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/price-rules/page.mdx @@ -8,7 +8,7 @@ In this document, you'll learn about price rules for price sets and price lists. ## Price Rule -Each rule of a price within a price set is represented by the [PriceRule data model](/references/pricing/models/PriceRule), which holds the value of a rule type. The `Price` data model has a `rules_count` field, which indicates how many rules, represented by `PriceRule`, are applied to the price. +Each rule of a price within a price set is represented by the [PriceRule data model](/references/pricing/models/PriceRule), which holds the value of a rule type. The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. ![A diagram showcasing the relation between the PriceRule, PriceSet, Price, and RuleType.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) @@ -40,7 +40,7 @@ For example, to use the `zip_code` rule type on a price in a price set, the rule ## Price List Rules -Rules that can be applied to a price list are represented by the [PriceListRule data model](/references/pricing/models/PriceListRule). The `rules_count` field of a `PriceList` indicates how many rules are applied to it. +Rules that can be applied to a price list are represented by the [PriceListRule data model](/references/pricing/models/PriceListRule). The `rules_count` property of a `PriceList` indicates how many rules are applied to it. Each rule of a price list can have more than one value, representing its values by the [PriceListRuleValue data model](/references/pricing/models/PriceListRuleValue). diff --git a/www/apps/resources/app/commerce-modules/product/examples/page.mdx b/www/apps/resources/app/commerce-modules/product/examples/page.mdx index cf6b773455..5da0d88d30 100644 --- a/www/apps/resources/app/commerce-modules/product/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/examples/page.mdx @@ -25,7 +25,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu const productModuleService: IProductModuleService = request.scope.resolve(ModuleRegistrationName.PRODUCT) - const products = await productModuleService.create([ + const products = await productModuleService.createProducts([ { title: "Medusa Shirt", options: [ @@ -63,7 +63,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu export async function POST(request: Request) { const productModuleService = await initializeProductModule() - const products = await productModuleService.create([ + const products = await productModuleService.createProducts([ { title: "Medusa Shirt", options: [ @@ -110,7 +110,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu const productModuleService: IProductModuleService = request.scope.resolve(ModuleRegistrationName.PRODUCT) - const products = await productModuleService.list() + const products = await productModuleService.listProducts() res.json({ products }) } @@ -129,7 +129,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu export async function GET(request: Request) { const productModuleService = await initializeProductModule() - const products = await productModuleService.list() + const products = await productModuleService.listProducts() return NextResponse.json({ products }) } @@ -157,7 +157,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu const productModuleService: IProductModuleService = request.scope.resolve(ModuleRegistrationName.PRODUCT) - const product = await productModuleService.retrieve( + const product = await productModuleService.retrieveProduct( request.params.id ) @@ -182,7 +182,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu const { id } = params const productModuleService = await initializeProductModule() - const product = await productModuleService.retrieve( + const product = await productModuleService.retrieveProduct( id ) @@ -212,7 +212,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu const productModuleService: IProductModuleService = request.scope.resolve(ModuleRegistrationName.PRODUCT) - const data = await productModuleService.list({ + const data = await productModuleService.listProducts({ handle: "shirt", }) @@ -233,7 +233,7 @@ In this guide, you’ll find common examples of how you can use the Product Modu export async function GET(request: Request) { const productModuleService = await initializeProductModule() - const data = await productModuleService.list({ + const data = await productModuleService.listProducts({ handle: "shirt", }) @@ -263,7 +263,8 @@ In this guide, you’ll find common examples of how you can use the Product Modu const productModuleService: IProductModuleService = request.scope.resolve(ModuleRegistrationName.PRODUCT) - const categories = await productModuleService.listCategories() + const categories = await productModuleService + .listProductCategories() res.json({ categories }) } @@ -282,7 +283,8 @@ In this guide, you’ll find common examples of how you can use the Product Modu export async function GET(request: Request) { const productModuleService = await initializeProductModule() - const categories = await productModuleService.listCategories() + const categories = await productModuleService + .listProductCategories() return NextResponse.json({ categories }) } @@ -310,9 +312,10 @@ In this guide, you’ll find common examples of how you can use the Product Modu const productModuleService: IProductModuleService = request.scope.resolve(ModuleRegistrationName.PRODUCT) - const data = await productModuleService.listCategories({ - handle: request.params.handle, - }) + const data = await productModuleService + .listProductCategories({ + handle: request.params.handle, + }) res.json({ category: data[0] }) } @@ -335,9 +338,10 @@ In this guide, you’ll find common examples of how you can use the Product Modu const { handle } = params const productModuleService = await initializeProductModule() - const data = await productModuleService.listCategories({ - handle, - }) + const data = await productModuleService + .listProductCategories({ + handle, + }) return NextResponse.json({ category: data[0] }) } diff --git a/www/apps/resources/app/commerce-modules/product/page.mdx b/www/apps/resources/app/commerce-modules/product/page.mdx index 0db257bb24..12ab3831de 100644 --- a/www/apps/resources/app/commerce-modules/product/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/page.mdx @@ -15,7 +15,7 @@ The Product Module is the `@medusajs/product` NPM package that provides product- Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. ```ts -const products = await productService.create([ +const products = await productService.createProducts([ { title: "Medusa Shirt", options: [ @@ -42,11 +42,11 @@ const products = await productService.create([ The Product Module provides different data models used to organize products, including categories, collections, tags, and more. ```ts -const category = await productService.createCategory({ +const category = await productService.createProductCategories({ name: "Shirts", }) -const products = await productService.update([ +const products = await productService.updateProducts([ { id: product.id, categories: [ @@ -81,7 +81,9 @@ For example: const productModuleService: IProductModuleService = request.scope.resolve(ModuleRegistrationName.PRODUCT) - res.json({ products: await productModuleService.list() }) + res.json({ + products: await productModuleService.listProducts(), + }) } ``` @@ -99,7 +101,7 @@ For example: const productModuleService: IProductModuleService = container.resolve(ModuleRegistrationName.PRODUCT) - const products = await productModuleService.list() + const products = await productModuleService.listProducts() } ``` @@ -117,7 +119,7 @@ For example: const productModuleService: IProductModuleService = container.resolve(ModuleRegistrationName.PRODUCT) - const products = await productModuleService.list() + const products = await productModuleService.listProducts() }) ``` diff --git a/www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx b/www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx index d9c559edec..bab8fcd996 100644 --- a/www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/relations-to-other-modules/page.mdx @@ -48,6 +48,6 @@ Each product variant has different inventory details. Medusa defines a link modu ![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) -When the `manage_inventory` field of a product variant is enabled, you can manage the variant's inventory in different locations through this relation. +When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation. Learn more about the `InventoryItem` data model in the [Inventory Concepts](../../inventory/concepts/page.mdx#inventoryitem) \ No newline at end of file diff --git a/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx b/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx index 1dec4e5c29..ef8578bb95 100644 --- a/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/actions/page.mdx @@ -10,7 +10,7 @@ In this document, you’ll learn about promotion actions and how they’re compu The Promotion Module's main service has a [computeActions method](/references/promotion/computeActions) that returns an array of actions to perform on a cart when one or more promotions are applied. -Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` field indicating the type of action. +Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. --- diff --git a/www/apps/resources/app/commerce-modules/promotion/application-method/page.mdx b/www/apps/resources/app/commerce-modules/promotion/application-method/page.mdx index 5b2124869f..04f9f05811 100644 --- a/www/apps/resources/app/commerce-modules/promotion/application-method/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/application-method/page.mdx @@ -17,7 +17,7 @@ The [ApplicationMethod data model](/references/promotion/models/ApplicationMetho - Field + Property @@ -71,7 +71,7 @@ The [ApplicationMethod data model](/references/promotion/models/ApplicationMetho When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. -The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` field represents this relation. +The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. ![A diagram showcasing the target_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) @@ -83,7 +83,7 @@ In this example, the promotion is only applied on products in the cart having th When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. -The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` field represents this relation. +The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. ![A diagram showcasing the buy_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) diff --git a/www/apps/resources/app/commerce-modules/promotion/concepts/page.mdx b/www/apps/resources/app/commerce-modules/promotion/concepts/page.mdx index ffa68a442a..b7706bccac 100644 --- a/www/apps/resources/app/commerce-modules/promotion/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/concepts/page.mdx @@ -78,19 +78,19 @@ A promotion can be restricted by a set of rules, each rule is represented by the ![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg) -A `PromotionRule`'s `attribute` field indicates the field's name to which this rule is applied. For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. +A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`. Its value is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. -When testing whether a promotion can be applied to a cart, the rule's `attribute` field and its values are tested on the cart itself. For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. +When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. --- ## Flexible Rules -The `PromotionRule`'s `operator` field adds more flexibility to the rule’s condition rather than simple equality (`eq`). +The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). For example, to restrict the promotion to only `VIP` and `B2B` customer groups: -- Add a `PromotionRule` with its `attribute` field set to `customer_group_id` and `operator` field to `in`. +- Add a `PromotionRule` with its `attribute` property set to `customer_group_id` and `operator` property to `in`. - Add two `PromotionRuleValue` associated with the rule: one with the value `VIP` and the other `B2B`. ![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg) diff --git a/www/apps/resources/app/commerce-modules/promotion/examples/page.mdx b/www/apps/resources/app/commerce-modules/promotion/examples/page.mdx index b810fb89a6..f8c369e9bd 100644 --- a/www/apps/resources/app/commerce-modules/promotion/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/examples/page.mdx @@ -25,7 +25,7 @@ In this document, you’ll find common examples of how you can use the Promotion const promotionModuleService: IPromotionModuleService = request.scope.resolve(ModuleRegistrationName.PROMOTION) - const promotion = await promotionModuleService.create({ + const promotion = await promotionModuleService.createPromotions({ code: "10%OFF", type: "standard", application_method: { @@ -55,7 +55,7 @@ In this document, you’ll find common examples of how you can use the Promotion await initializePromotionModule() const body = await request.json() - const promotion = await promotionModuleService.create({ + const promotion = await promotionModuleService.createPromotions({ code: "10%OFF", type: "standard", application_method: { @@ -155,7 +155,7 @@ In this document, you’ll find common examples of how you can use the Promotion const promotionModuleService: IPromotionModuleService = request.scope.resolve(ModuleRegistrationName.PROMOTION) - const promotion = await promotionModuleService.create({ + const promotion = await promotionModuleService.createPromotions({ code: "10%OFF", type: "standard", application_method: { @@ -191,7 +191,7 @@ In this document, you’ll find common examples of how you can use the Promotion const promotionModuleService = await initializePromotionModule() - const promotion = await promotionModuleService.create({ + const promotion = await promotionModuleService.createPromotions({ code: "10%OFF", type: "standard", application_method: { @@ -236,7 +236,7 @@ In this document, you’ll find common examples of how you can use the Promotion request.scope.resolve(ModuleRegistrationName.PROMOTION) res.json({ - promotions: await promotionModuleService.list(), + promotions: await promotionModuleService.listPromotions(), }) } ``` @@ -256,7 +256,7 @@ In this document, you’ll find common examples of how you can use the Promotion await initializePromotionModule() return NextResponse.json({ - promotions: await promotionModuleService.list(), + promotions: await promotionModuleService.listPromotions(), }) } ``` diff --git a/www/apps/resources/app/commerce-modules/promotion/page.mdx b/www/apps/resources/app/commerce-modules/promotion/page.mdx index 612ef558b6..9063556876 100644 --- a/www/apps/resources/app/commerce-modules/promotion/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/page.mdx @@ -17,7 +17,7 @@ A promotion discounts an amount or percentage of a cart's items, shipping method The Promotion Module allows you to store and manage promotions. ```ts -const promotion = await promotionModuleService.create({ +const promotion = await promotionModuleService.createPromotions({ code: "10%OFF", type: "standard", application_method: { @@ -34,7 +34,7 @@ const promotion = await promotionModuleService.create({ A promotion has rules that restricts when it's applied. For example, you can create a promotion that's only applied to VIP customers. ```ts -const promotion = await promotionModuleService.create({ +const promotion = await promotionModuleService.createPromotions({ code: "10%OFF", type: "standard", application_method: { @@ -94,7 +94,7 @@ For example: request.scope.resolve(ModuleRegistrationName.PROMOTION) res.json({ - promotions: await promotionModuleService.list(), + promotions: await promotionModuleService.listPromotions(), }) } ``` @@ -113,7 +113,7 @@ For example: const promotionModuleService: IPromotionModuleService = container.resolve(ModuleRegistrationName.PROMOTION) - const promotions = await promotionModuleService.list() + const promotions = await promotionModuleService.listPromotions() } ``` @@ -131,7 +131,7 @@ For example: const promotionModuleService: IPromotionModuleService = container.resolve(ModuleRegistrationName.PROMOTION) - const promotions = await promotionModuleService.list() + const promotions = await promotionModuleService.listPromotions() }) ``` diff --git a/www/apps/resources/app/commerce-modules/region/examples/page.mdx b/www/apps/resources/app/commerce-modules/region/examples/page.mdx index 39592e078c..77c270c80d 100644 --- a/www/apps/resources/app/commerce-modules/region/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/region/examples/page.mdx @@ -25,7 +25,7 @@ In this guide, you’ll find common examples of how you can use the Region Modul const regionModuleService: IRegionModuleService = req.scope.resolve(ModuleRegistrationName.REGION) - const region = await regionModuleService.create({ + const region = await regionModuleService.createRegions({ name: "Europe", currency_code: "eur", }) @@ -49,7 +49,7 @@ In this guide, you’ll find common examples of how you can use the Region Modul export async function POST(request: Request) { const regionModuleService = await initializeRegionModule() - const region = await regionModuleService.create({ + const region = await regionModuleService.createRegions({ name: "Europe", currency_code: "eur", }) @@ -83,7 +83,7 @@ In this guide, you’ll find common examples of how you can use the Region Modul req.scope.resolve(ModuleRegistrationName.REGION) res.json({ - regions: await regionModuleService.list(), + regions: await regionModuleService.listRegions(), }) } ``` @@ -102,7 +102,7 @@ In this guide, you’ll find common examples of how you can use the Region Modul const regionModuleService = await initializeRegionModule() return NextResponse.json({ - regions: await regionModuleService.list(), + regions: await regionModuleService.listRegions(), }) } ``` @@ -129,7 +129,8 @@ In this guide, you’ll find common examples of how you can use the Region Modul const regionModuleService: IRegionModuleService = req.scope.resolve(ModuleRegistrationName.REGION) - const region = await regionModuleService.retrieve("reg_123") + const region = await regionModuleService + .retrieveRegion("reg_123") res.json({ region }) } @@ -150,7 +151,8 @@ In this guide, you’ll find common examples of how you can use the Region Modul ) { const regionModuleService = await initializeRegionModule() - const region = await regionModuleService.retrieve("reg_123") + const region = await regionModuleService + .retrieveRegion("reg_123") return NextResponse.json({ region }) } @@ -178,9 +180,10 @@ In this guide, you’ll find common examples of how you can use the Region Modul const regionModuleService: IRegionModuleService = req.scope.resolve(ModuleRegistrationName.REGION) - const region = await regionModuleService.update("reg_123", { - automatic_taxes: false, - }) + const region = await regionModuleService + .updateRegions("reg_123", { + automatic_taxes: false, + }) res.json({ region }) } @@ -201,9 +204,10 @@ In this guide, you’ll find common examples of how you can use the Region Modul ) { const regionModuleService = await initializeRegionModule() - const region = await regionModuleService.update("reg_123", { - automatic_taxes: false, - }) + const region = await regionModuleService + .updateRegions("reg_123", { + automatic_taxes: false, + }) return NextResponse.json({ region }) } @@ -231,7 +235,7 @@ In this guide, you’ll find common examples of how you can use the Region Modul const regionModuleService: IRegionModuleService = req.scope.resolve(ModuleRegistrationName.REGION) - await regionModuleService.delete("reg_123") + await regionModuleService.deleteRegions("reg_123") res.status(200) } @@ -252,7 +256,7 @@ In this guide, you’ll find common examples of how you can use the Region Modul ) { const regionModuleService = await initializeRegionModule() - await regionModuleService.delete("reg_123") + await regionModuleService.deleteRegions("reg_123") } ``` diff --git a/www/apps/resources/app/commerce-modules/region/page.mdx b/www/apps/resources/app/commerce-modules/region/page.mdx index 9ea32d7d86..90fcc9b857 100644 --- a/www/apps/resources/app/commerce-modules/region/page.mdx +++ b/www/apps/resources/app/commerce-modules/region/page.mdx @@ -21,7 +21,7 @@ A region represents the area you sell products in. Each region can cover multipl You can manage your regions to create, update, retrieve, or delete them. ```ts -const region = await regionModuleService.create({ +const region = await regionModuleService.createRegions({ name: "Europe", currency_code: "eur", }) @@ -32,7 +32,7 @@ const region = await regionModuleService.create({ As each region has a currency, you can support multiple currencies in your store by creating multiple regions. ```ts -const regions = await regionModuleService.create([ +const regions = await regionModuleService.createRegions([ { name: "Europe", currency_code: "eur", @@ -49,7 +49,7 @@ const regions = await regionModuleService.create([ Each region has its own settings, such as what countries belong to a region or its tax settings. Each region has different tax rates, payment providers, and more provided by other commerce modules. ```ts -const regions = await regionModuleService.create([ +const regions = await regionModuleService.createRegions([ { name: "Europe", currency_code: "eur", @@ -89,7 +89,7 @@ For example: req.scope.resolve(ModuleRegistrationName.REGION) res.json({ - regions: await regionModuleService.list(), + regions: await regionModuleService.listRegions(), }) } ``` @@ -108,7 +108,7 @@ For example: const regionModuleService: IRegionModuleService = container.resolve(ModuleRegistrationName.REGION) - const regions = await regionModuleService.list() + const regions = await regionModuleService.listRegions() } ``` @@ -128,7 +128,7 @@ For example: ModuleRegistrationName.REGION ) - const regions = await regionModuleService.list() + const regions = await regionModuleService.listRegions() }) ``` diff --git a/www/apps/resources/app/commerce-modules/sales-channel/examples/page.mdx b/www/apps/resources/app/commerce-modules/sales-channel/examples/page.mdx index e32e55df3b..1440e2910e 100644 --- a/www/apps/resources/app/commerce-modules/sales-channel/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/sales-channel/examples/page.mdx @@ -25,9 +25,10 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService: ISalesChannelModuleService = request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL) - const salesChannel = await salesChannelModuleService.create({ - name: "B2B", - }) + const salesChannel = await salesChannelModuleService + .createSalesChannels({ + name: "B2B", + }) res.json({ sales_channel: salesChannel, @@ -49,9 +50,10 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService = await initializeSalesChannelModule() - const salesChannel = await salesChannelModuleService.create({ - name: "B2B", - }) + const salesChannel = await salesChannelModuleService + .createSalesChannels({ + name: "B2B", + }) return NextResponse.json({ sales_channel: salesChannel }) } @@ -80,7 +82,8 @@ In this guide, you’ll find common examples of how you can use the Sales Channe request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL) res.json({ - sales_channels: salesChannelModuleService.list(), + sales_channels: salesChannelModuleService + .listSalesChannels(), }) } ``` @@ -99,7 +102,8 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService = await initializeSalesChannelModule() - const salesChannels = await salesChannelModuleService.list() + const salesChannels = await salesChannelModuleService + .listSalesChannels() return NextResponse.json({ sales_channels: salesChannels }) } @@ -127,9 +131,10 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService: ISalesChannelModuleService = request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL) - const salesChannel = await salesChannelModuleService.retrieve( - "sc_123" - ) + const salesChannel = await salesChannelModuleService + .retrieveSalesChannel( + "sc_123" + ) res.json({ sales_channel: salesChannel, @@ -153,9 +158,10 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService = await initializeSalesChannelModule() - const salesChannel = await salesChannelModuleService.retrieve( - "sc_123" - ) + const salesChannel = await salesChannelModuleService + .retrieveSalesChannel( + "sc_123" + ) return NextResponse.json({ sales_channel: salesChannel }) } @@ -183,10 +189,11 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService: ISalesChannelModuleService = request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL) - const salesChannel = await salesChannelModuleService.update({ - id: "sc_123", - description: "Sales channel for B2B customers", - }) + const salesChannel = await salesChannelModuleService + .updateSalesChannels({ + id: "sc_123", + description: "Sales channel for B2B customers", + }) res.json({ sales_channel: salesChannel, @@ -210,10 +217,11 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService = await initializeSalesChannelModule() - const salesChannel = await salesChannelModuleService.update({ - id: "sc_123", - description: "Sales channel for B2B customers", - }) + const salesChannel = await salesChannelModuleService + .updateSalesChannels({ + id: "sc_123", + description: "Sales channel for B2B customers", + }) return NextResponse.json({ sales_channel: salesChannel }) } @@ -241,7 +249,7 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService: ISalesChannelModuleService = request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL) - await salesChannelModuleService.delete("sc_123") + await salesChannelModuleService.deleteSalesChannels("sc_123") res.status(200) } @@ -263,7 +271,7 @@ In this guide, you’ll find common examples of how you can use the Sales Channe const salesChannelModuleService = await initializeSalesChannelModule() - await salesChannelModuleService.delete("sc_123") + await salesChannelModuleService.deleteSalesChannels("sc_123") } ``` diff --git a/www/apps/resources/app/commerce-modules/sales-channel/page.mdx b/www/apps/resources/app/commerce-modules/sales-channel/page.mdx index 50f213ae1f..a6e9abc789 100644 --- a/www/apps/resources/app/commerce-modules/sales-channel/page.mdx +++ b/www/apps/resources/app/commerce-modules/sales-channel/page.mdx @@ -27,14 +27,15 @@ Some use case examples for using a sales channel: Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels. ```ts -const salesChannels = await salesChannelModuleService.create([ - { - name: "B2B", - }, - { - name: "Mobile App", - }, -]) +const salesChannels = await salesChannelModuleService + .createSalesChannels([ + { + name: "B2B", + }, + { + name: "Mobile App", + }, + ]) ``` ### Product Availability @@ -73,7 +74,8 @@ For example: request.scope.resolve(ModuleRegistrationName.SALES_CHANNEL) res.json({ - sales_channels: await salesChannelModuleService.list(), + sales_channels: await salesChannelModuleService + .listSalesChannels(), }) } ``` @@ -92,7 +94,8 @@ For example: const salesChannelModuleService: ISalesChannelModuleService = container.resolve(ModuleRegistrationName.SALES_CHANNEL) - const salesChannels = await salesChannelModuleService.list() + const salesChannels = await salesChannelModuleService + .listSalesChannels() } ``` @@ -112,7 +115,8 @@ For example: ModuleRegistrationName.SALES_CHANNEL ) - const salesChannels = await salesChannelModuleService.list() + const salesChannels = await salesChannelModuleService + .listSalesChannels() }) ``` diff --git a/www/apps/resources/app/commerce-modules/stock-location/examples/page.mdx b/www/apps/resources/app/commerce-modules/stock-location/examples/page.mdx index 65c122f185..9c184efa22 100644 --- a/www/apps/resources/app/commerce-modules/stock-location/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/stock-location/examples/page.mdx @@ -25,11 +25,12 @@ In this document, you’ll find common examples of how you can use the Stock Loc const stockLocationModuleService: IStockLocationService = request.scope.resolve(ModuleRegistrationName.STOCK_LOCATION) - const stockLocation = await stockLocationModuleService.create( - { - name: "Warehouse 1", - } - ) + const stockLocation = await stockLocationModuleService + .createStockLocations( + { + name: "Warehouse 1", + } + ) res.json({ stock_location: stockLocation, @@ -51,11 +52,12 @@ In this document, you’ll find common examples of how you can use the Stock Loc const stockLocationModuleService = await initializeStockLocationModule({}) - const stockLocation = await stockLocationModuleService.create( - { - name: "Warehouse 1", - } - ) + const stockLocation = await stockLocationModuleService + .createStockLocations( + { + name: "Warehouse 1", + } + ) return NextResponse.json({ stock_location: stockLocation }) } @@ -84,7 +86,8 @@ In this document, you’ll find common examples of how you can use the Stock Loc request.scope.resolve(ModuleRegistrationName.STOCK_LOCATION) res.json({ - stock_locations: await stockLocationModuleService.list({}), + stock_locations: await stockLocationModuleService + .listStockLocations({}), }) } ``` @@ -104,7 +107,8 @@ In this document, you’ll find common examples of how you can use the Stock Loc await initializeStockLocationModule({}) return NextResponse.json({ - stock_locations: await stockLocationModuleService.list({}), + stock_locations: await stockLocationModuleService + .listStockLocations({}), }) } ``` @@ -131,15 +135,16 @@ In this document, you’ll find common examples of how you can use the Stock Loc const stockLocationModuleService: IStockLocationService = request.scope.resolve(ModuleRegistrationName.STOCK_LOCATION) - const stockLocation = await stockLocationModuleService.update({ - id: "sloc_123", - address: { - country_code: "US", - city: "New York City", - address_1: "52 Stone St", - postal_code: "10004", - }, - }) + const stockLocation = await stockLocationModuleService + .updateStockLocations({ + id: "sloc_123", + address: { + country_code: "US", + city: "New York City", + address_1: "52 Stone St", + postal_code: "10004", + }, + }) res.json({ stock_location: stockLocation, @@ -163,15 +168,16 @@ In this document, you’ll find common examples of how you can use the Stock Loc const stockLocationModuleService = await initializeStockLocationModule({}) - const stockLocation = await stockLocationModuleService.update({ - id: "sloc_123", - address: { - country_code: "us", - city: "New York City", - address_1: "52 Stone St", - postal_code: "10004", - }, - }) + const stockLocation = await stockLocationModuleService + .updateStockLocations({ + id: "sloc_123", + address: { + country_code: "us", + city: "New York City", + address_1: "52 Stone St", + postal_code: "10004", + }, + }) return NextResponse.json({ stock_location: stockLocation, @@ -201,7 +207,9 @@ In this document, you’ll find common examples of how you can use the Stock Loc const stockLocationModuleService: IStockLocationService = request.scope.resolve(ModuleRegistrationName.STOCK_LOCATION) - await stockLocationModuleService.delete("sloc_123") + await stockLocationModuleService.deleteStockLocations( + "sloc_123" + ) res.status(200) } @@ -223,7 +231,9 @@ In this document, you’ll find common examples of how you can use the Stock Loc const stockLocationModuleService = await initializeStockLocationModule({}) - await stockLocationModuleService.delete("sloc_123") + await stockLocationModuleService.deleteStockLocations( + "sloc_123" + ) } ``` diff --git a/www/apps/resources/app/commerce-modules/stock-location/page.mdx b/www/apps/resources/app/commerce-modules/stock-location/page.mdx index 12bcaff6c0..9d057ebe71 100644 --- a/www/apps/resources/app/commerce-modules/stock-location/page.mdx +++ b/www/apps/resources/app/commerce-modules/stock-location/page.mdx @@ -15,11 +15,12 @@ The Stock Location Module is the `@medusajs/stock-location-next` NPM package tha Store and manage stock locations. Stock locations are associated with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](../inventory/concepts/page.mdx#inventory-level). ```ts -const stockLocation = await stockLocationModuleService.create( - { +const stockLocation = await stockLocationModuleService + .createStockLocations( + { name: "Warehouse 1", - } -) + } + ) ``` ### Address Management @@ -27,17 +28,18 @@ const stockLocation = await stockLocationModuleService.create( Manage the address of each stock location. ```ts -const stockLocation = await stockLocationModuleService.update( - { - id: "sloc_123", - address: { - country_code: "us", - city: "New York City", - address_1: "52 Stone St", - postal_code: "10004", - }, - } -) +const stockLocation = await stockLocationModuleService + .updateStockLocations( + { + id: "sloc_123", + address: { + country_code: "us", + city: "New York City", + address_1: "52 Stone St", + postal_code: "10004", + }, + } + ) ``` --- @@ -64,7 +66,8 @@ For example: request.scope.resolve(ModuleRegistrationName.STOCK_LOCATION) res.json({ - stock_locations: await stockLocationModuleService.list({}), + stock_locations: await stockLocationModuleService + .listStockLocations({}), }) } ``` @@ -83,9 +86,8 @@ For example: const stockLocationModuleService: IStockLocationService = container.resolve(ModuleRegistrationName.STOCK_LOCATION) - const stockLocations = await stockLocationModuleService.list( - {} - ) + const stockLocations = await stockLocationModuleService + .listStockLocations({}) } ``` @@ -105,9 +107,8 @@ For example: ModuleRegistrationName.STOCK_LOCATION ) - const stockLocations = await stockLocationModuleService.list( - {} - ) + const stockLocations = await stockLocationModuleService + .listStockLocations({}) }) ``` diff --git a/www/apps/resources/app/commerce-modules/store/examples/page.mdx b/www/apps/resources/app/commerce-modules/store/examples/page.mdx index 3a8d4ffda3..abdfb6e840 100644 --- a/www/apps/resources/app/commerce-modules/store/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/store/examples/page.mdx @@ -25,9 +25,14 @@ In this guide, you’ll find common examples of how you can use the Store Module const storeModuleService: IStoreModuleService = request.scope.resolve(ModuleRegistrationName.STORE) - const store = await storeModuleService.create({ + const store = await storeModuleService.createStores({ name: "My Store", - supported_currency_codes: ["usd"], + supported_currencies: [ + { + currency_code: "usd", + is_default: true, + }, + ], }) res.json({ @@ -49,9 +54,14 @@ In this guide, you’ll find common examples of how you can use the Store Module export async function POST(request: Request) { const storeModuleService = await initializeStoreModule() - const store = await storeModuleService.create({ + const store = await storeModuleService.createStores({ name: "My Store", - supported_currency_codes: ["usd"], + supported_currencies: [ + { + currency_code: "usd", + is_default: true, + }, + ], }) res.json({ @@ -83,7 +93,7 @@ In this guide, you’ll find common examples of how you can use the Store Module request.scope.resolve(ModuleRegistrationName.STORE) res.json({ - stores: await storeModuleService.list(), + stores: await storeModuleService.listStores(), }) } ``` @@ -101,7 +111,7 @@ In this guide, you’ll find common examples of how you can use the Store Module export async function GET(request: Request) { const storeModuleService = await initializeStoreModule() - const salesChannels = await storeModuleService.list() + const salesChannels = await storeModuleService.listStores() return NextResponse.json({ stores: await storeModuleService.list(), @@ -131,7 +141,7 @@ In this guide, you’ll find common examples of how you can use the Store Module const storeModuleService: IStoreModuleService = request.scope.resolve(ModuleRegistrationName.STORE) - const store = await storeModuleService.retrieve( + const store = await storeModuleService.retrieveStore( "store_123" ) @@ -157,7 +167,7 @@ In this guide, you’ll find common examples of how you can use the Store Module ) { const storeModuleService = await initializeStoreModule() - const store = await storeModuleService.retrieve( + const store = await storeModuleService.retrieveStore( "store_123" ) @@ -187,9 +197,10 @@ In this guide, you’ll find common examples of how you can use the Store Module const storeModuleService: IStoreModuleService = request.scope.resolve(ModuleRegistrationName.STORE) - const store = await storeModuleService.update("store_123", { - name: "Change Store", - }) + const store = await storeModuleService + .updateStores("store_123", { + name: "Change Store", + }) res.json({ store, @@ -213,9 +224,10 @@ In this guide, you’ll find common examples of how you can use the Store Module ) { const storeModuleService = await initializeStoreModule() - const store = await storeModuleService.update("store_123", { - name: "Change Store", - }) + const store = await storeModuleService + .updateStores("store_123", { + name: "Change Store", + }) return NextResponse.json({ store }) } @@ -243,7 +255,7 @@ In this guide, you’ll find common examples of how you can use the Store Module const storeModuleService: IStoreModuleService = request.scope.resolve(ModuleRegistrationName.STORE) - await storeModuleService.delete("store_123") + await storeModuleService.deleteStores("store_123") res.status(200) } @@ -265,7 +277,7 @@ In this guide, you’ll find common examples of how you can use the Store Module ) { const storeModuleService = await initializeStoreModule() - await storeModuleService.delete("store_123") + await storeModuleService.deleteStores("store_123") } ``` diff --git a/www/apps/resources/app/commerce-modules/store/page.mdx b/www/apps/resources/app/commerce-modules/store/page.mdx index a94edd09c4..141e3ef1ba 100644 --- a/www/apps/resources/app/commerce-modules/store/page.mdx +++ b/www/apps/resources/app/commerce-modules/store/page.mdx @@ -15,7 +15,7 @@ The Store Module is the `@medusajs/store` NPM package that provides store-relate A store holds the main configurations of your commerce store, such as supported currencies, default region and sales channel, and more. ```ts -const store = await storeModuleService.create({ +const store = await storeModuleService.createStores({ name: "My Store", supported_currency_codes: ["usd"], }) @@ -26,7 +26,7 @@ const store = await storeModuleService.create({ You can create multiple stores, each having its own configurations. ```ts -const stores = await storeModuleService.create([ +const stores = await storeModuleService.createStores([ { name: "USA Store", supported_currency_codes: ["usd"], @@ -62,7 +62,7 @@ For example: request.scope.resolve(ModuleRegistrationName.STORE) res.json({ - stores: await storeModuleService.list(), + stores: await storeModuleService.listStores(), }) } ``` @@ -81,7 +81,7 @@ For example: const storeModuleService: IStoreModuleService = container.resolve(ModuleRegistrationName.STORE) - const stores = await storeModuleService.list() + const stores = await storeModuleService.listStores() } ``` @@ -101,7 +101,7 @@ For example: ModuleRegistrationName.STORE ) - const stores = await storeModuleService.list() + const stores = await storeModuleService.listStores() }) ``` diff --git a/www/apps/resources/app/commerce-modules/tax/examples/page.mdx b/www/apps/resources/app/commerce-modules/tax/examples/page.mdx index 51569dcf8e..5a54c063b7 100644 --- a/www/apps/resources/app/commerce-modules/tax/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/tax/examples/page.mdx @@ -91,7 +91,7 @@ In this guide, you’ll find common examples of how you can use the Tax Module i ) res.json({ - tax_regions: await taxModuleService.list(), + tax_regions: await taxModuleService.listTaxRegions(), }) } ``` @@ -110,7 +110,7 @@ In this guide, you’ll find common examples of how you can use the Tax Module i const taxModuleService = await initializeTaxModule() return NextResponse.json({ - tax_regions: await taxModuleService.list(), + tax_regions: await taxModuleService.listTaxRegions(), }) } ``` @@ -138,7 +138,7 @@ In this guide, you’ll find common examples of how you can use the Tax Module i ModuleRegistrationName.TAX ) - const taxRate = await taxModuleService.create({ + const taxRate = await taxModuleService.createTaxRates({ tax_region_id: "txreg_123", name: "Custom rate", rate: 15, @@ -175,7 +175,7 @@ In this guide, you’ll find common examples of how you can use the Tax Module i ) { const taxModuleService = await initializeTaxModule() - const taxRate = await taxModuleService.create({ + const taxRate = await taxModuleService.createTaxRates({ tax_region_id: "txreg_123", name: "Custom rate", rate: 15, @@ -221,7 +221,7 @@ In this guide, you’ll find common examples of how you can use the Tax Module i ) res.json({ - tax_rates: await taxModuleService.list(), + tax_rates: await taxModuleService.listTaxRates(), }) } ``` @@ -242,7 +242,7 @@ In this guide, you’ll find common examples of how you can use the Tax Module i const taxModuleService = await initializeTaxModule() return NextResponse.json({ - tax_rates: await taxModuleService.list(), + tax_rates: await taxModuleService.listTaxRates(), }) } ``` diff --git a/www/apps/resources/app/commerce-modules/tax/page.mdx b/www/apps/resources/app/commerce-modules/tax/page.mdx index 3ec1aec2df..f0b43f9cab 100644 --- a/www/apps/resources/app/commerce-modules/tax/page.mdx +++ b/www/apps/resources/app/commerce-modules/tax/page.mdx @@ -25,7 +25,7 @@ const taxRegion = await taxModuleService.createTaxRegions({ Manage each region's default tax rates and override them with conditioned tax rates. ```ts -const taxRates = await taxModuleService.create([ +const taxRates = await taxModuleService.createTaxRates([ { tax_region_id: "txreg_123", name: "Default tax rate", diff --git a/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/page.mdx b/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/page.mdx index 0a76925c7c..5412ca36ca 100644 --- a/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/page.mdx +++ b/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/page.mdx @@ -35,7 +35,7 @@ const taxLines = await taxModuleService.getTaxLines( ) ``` -The context object is used to determine which tax regions and rates to use in the calculation. It includes fields related to the address and customer. +The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer. The example above returns the tax lines based on the tax region for the United States. diff --git a/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/page.mdx b/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/page.mdx index 43d33c66c3..9c4c5f7d3b 100644 --- a/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/page.mdx +++ b/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/page.mdx @@ -28,7 +28,7 @@ A tax region can have multiple tax rates, and each tax rate can have multiple ta ![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) -These two fields of the data model identify the rule’s target: +These two properties of the data model identify the rule’s target: - `reference`: the name of the table in the database that this rule points to. For example, `product_type`. - `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. diff --git a/www/apps/resources/app/commerce-modules/user/examples/page.mdx b/www/apps/resources/app/commerce-modules/user/examples/page.mdx index aee2e27d1a..119fcaade5 100644 --- a/www/apps/resources/app/commerce-modules/user/examples/page.mdx +++ b/www/apps/resources/app/commerce-modules/user/examples/page.mdx @@ -25,7 +25,7 @@ In this guide, you’ll find common examples of how you can use the User Module const userModuleService: IUserModuleService = req.scope.resolve(ModuleRegistrationName.USER) - const user = await userModuleService.create({ + const user = await userModuleService.createUsers({ email: "user@example.com", first_name: "John", last_name: "Smith", @@ -48,7 +48,7 @@ In this guide, you’ll find common examples of how you can use the User Module export async function POST(request: Request) { const userModuleService = await initializeUserModule() - const user = await userModuleService.create({ + const user = await userModuleService.createUsers({ email: "user@example.com", first_name: "John", last_name: "Smith", @@ -83,7 +83,7 @@ In this guide, you’ll find common examples of how you can use the User Module req.scope.resolve(ModuleRegistrationName.USER) res.json({ - users: await userModuleService.list(), + users: await userModuleService.listUsers(), }) } ``` @@ -102,7 +102,7 @@ In this guide, you’ll find common examples of how you can use the User Module const userModuleService = await initializeUserModule() return NextResponse.json({ - users: await userModuleService.list(), + users: await userModuleService.listUsers(), }) } ``` @@ -129,7 +129,7 @@ In this guide, you’ll find common examples of how you can use the User Module const userModuleService: IUserModuleService = req.scope.resolve(ModuleRegistrationName.USER) - const user = await userModuleService.update({ + const user = await userModuleService.updateUsers({ id: "user_123", last_name: "Smith", }) @@ -153,7 +153,7 @@ In this guide, you’ll find common examples of how you can use the User Module ) { const userModuleService = await initializeUserModule() - const user = await userModuleService.update({ + const user = await userModuleService.updateUsers({ id: "user_123", last_name: "Smith", }) @@ -186,7 +186,7 @@ In this guide, you’ll find common examples of how you can use the User Module const userModuleService: IUserModuleService = req.scope.resolve(ModuleRegistrationName.USER) - await userModuleService.delete(["user_123"]) + await userModuleService.deleteUsers(["user_123"]) } ``` @@ -205,7 +205,7 @@ In this guide, you’ll find common examples of how you can use the User Module ) { const userModuleService = await initializeUserModule() - await userModuleService.delete(["user_123"]) + await userModuleService.deleteUsers(["user_123"]) } ``` @@ -292,7 +292,7 @@ In this guide, you’ll find common examples of how you can use the User Module const invite = await userModuleService.validateInviteToken("secret_123") - const user = await userModuleService.create({ + const user = await userModuleService.createUsers({ email: invite.email, metadata: invite.metadata, }) @@ -327,7 +327,7 @@ In this guide, you’ll find common examples of how you can use the User Module const invite = await userModuleService.validateInviteToken("secret_123") - const user = await userModuleService.create({ + const user = await userModuleService.createUsers({ email: invite.email, metadata: invite.metadata, }) diff --git a/www/apps/resources/app/commerce-modules/user/page.mdx b/www/apps/resources/app/commerce-modules/user/page.mdx index d1f9a09925..fd3618f596 100644 --- a/www/apps/resources/app/commerce-modules/user/page.mdx +++ b/www/apps/resources/app/commerce-modules/user/page.mdx @@ -15,7 +15,7 @@ The User Module is the `@medusajs/user` NPM package that provides user-related f Manage and store your users through Create, Read, Update, and Delete (CRUD) operations: ```ts -const user = await userModuleService.create({ +const user = await userModuleService.createUsers({ email: "user@example.com", first_name: "John", last_name: "Smith", @@ -65,7 +65,7 @@ For example: req.scope.resolve(ModuleRegistrationName.USER) res.json({ - users: await userModuleService.list(), + users: await userModuleService.listUsers(), }) } ``` @@ -84,7 +84,7 @@ For example: const userModuleService: IUserModuleService = container.resolve(ModuleRegistrationName.USER) - const users = await userModuleService.list() + const users = await userModuleService.listUsers() } ``` @@ -104,7 +104,7 @@ For example: ModuleRegistrationName.USER ) - const users = await userModuleService.list() + const users = await userModuleService.listUsers() }) ``` diff --git a/www/apps/resources/app/commerce-modules/user/user-creation-flows/page.mdx b/www/apps/resources/app/commerce-modules/user/user-creation-flows/page.mdx index 0778e1af70..8d96597f91 100644 --- a/www/apps/resources/app/commerce-modules/user/user-creation-flows/page.mdx +++ b/www/apps/resources/app/commerce-modules/user/user-creation-flows/page.mdx @@ -25,7 +25,7 @@ await userModuleService.updateInvites({ accepted: true, }) -const user = await userModuleService.create({ +const user = await userModuleService.createUsers({ email: invite.email, }) ``` @@ -45,7 +45,7 @@ await userModuleService.refreshInviteTokens(["invite_123"]) Finally, you can create a user using the [create method of the User Module’s main service](/references/user/create): ```ts -const user = await userModuleService.create({ +const user = await userModuleService.createUsers({ email: "user@example.com", }) ``` @@ -63,12 +63,12 @@ const { success, authIdentity } = }) // assuming authIdentity is defined -const [, count] = await userModuleService.listAndCount({ +const [, count] = await userModuleService.listAndCountUsers({ email: authIdentity.entity_id, }) if (!count) { - const user = await userModuleService.create({ + const user = await userModuleService.createUsers({ email: authIdentity.entity_id, }) } diff --git a/www/apps/resources/app/nextjs-starter/page.mdx b/www/apps/resources/app/nextjs-starter/page.mdx index f4a4be917f..31af978884 100644 --- a/www/apps/resources/app/nextjs-starter/page.mdx +++ b/www/apps/resources/app/nextjs-starter/page.mdx @@ -95,13 +95,11 @@ The Next.js Starter storefront is compatible with Medusa's Stripe and PayPal plu ### Stripe Integration -{/* TODO add links to stripe module once supported. */} + -{/* +- [Stripe provider module](../commerce-modules/payment/payment-provider/stripe/page.mdx) installed in the Medusa application. -- [Stripe plugin](../plugins/payment/stripe/page.mdx) installed in the Medusa application. - - */} + In your Next.js Starter project, set the following environment variables for the Stripe integration: @@ -113,167 +111,6 @@ Where `` is your Stripe publishable key. You can retrieve Then, restart the Next.js Starter storefront. You can now use Stripe during checkout. -### PayPal Integration - -{/* TODO add links to paypal module once supported. */} - -{/* - -- [PayPal plugin](../plugins/payment/paypal/page.mdx) installed in the Medusa application. - - */} - -In your Next.js Starter project, set the following environment variables for the PayPal integration: - -```bash -NEXT_PUBLIC_PAYPAL_CLIENT_ID= -``` - -Where `` is your PayPal client ID. You can retrieve it from the [PayPal developer dashboard](https://developer.paypal.com/developer/applications/). - -Then, restart the Next.js Starter storefront. You can now use PayPal during checkout. - ---- - -## Search Engine - -The Next.js Starter storefront is compatible with Medusa's MeiliSearch and Algolia plugins. - -### Enable Search Engine - -To enable the search engine, change the value of the feature in `store.config.json`: - -```json -{ - "features": { - "search": false - } -} -``` - -Then, restart your Next.js storefront. You'll see a "Search" link in the navigation bar. - -### Configure MeiliSearch - -{/* TODO add link to meilisearch plugin/module once available. */} - -{/* - -- [MeiliSearch plugin](../plugins/search/meilisearch/page.mdx) installed in the Medusa application. - - */} - -In your Next.js Starter storefront, set the following environment variables for the MeiliSearch integration: - -```bash -NEXT_PUBLIC_SEARCH_ENDPOINT= -NEXT_PUBLIC_INDEX_NAME=products -NEXT_PUBLIC_SEARCH_API_KEY= -``` - -Where: - -- `` is the URL MeiliSearch is running on. The default is `http://127.0.0.1:7700`. -- `NEXT_PUBLIC_INDEX_NAME` is the index name of the products in MeiliSearch. By default, it’s `products`. -- `` is the API key used to search through MeiliSearch indexes. To create a new API Key, make sure that the MeiliSearch service is running and send the following request: - -```bash -curl \ - -X POST '/keys' \ - -H 'Content-Type: application/json' \ - -H 'Authorization: Bearer ' \ - --data-binary '{ - "description": "Search products", - "actions": ["search"], - "indexes": ["products"], - "expiresAt": "2024-01-01T00:00:00Z" - }' -``` - -Make sure to replace `` with the URL MeiliSearch is running on and `` with your MeiliSearch master key. - -Then, restart the Next.js Starter storefront. You can search through available products by clicking the search icon in the navigation bar. - - - -To make sure the Next.js Starter storefront properly displays the products in the search result, include in the `displayedAttributes` setting of the MeiliSearch plugin on the Medusa application at least the fields `title`, `handle`, `description`, and `thumbnail`. - - - -### Configure Algolia - -{/* TODO add links to algolia plugin/module once available. */} - -{/* - -- [Algolia plugin](../plugins/search/algolia/page.mdx) installed in the Medusa application. - - */} - -In your Next.js Starter's directory, install the `algoliasearch` package: - -```bash npm2yarn -npm install algoliasearch -``` - -Then, set the following environment variables for the Algolia integration: - -```bash -NEXT_PUBLIC_SEARCH_APP_ID= -NEXT_PUBLIC_SEARCH_API_KEY= -NEXT_PUBLIC_INDEX_NAME=products -``` - -Where: - -- `` and `` are the Algolia App ID and Algolia Search API Key respectively. You can retrieve them from Algolia by going to [API Keys in your account settings](https://www.algolia.com/account/api-keys/all). -- `NEXT_PUBLIC_INDEX_NAME` is the index name of the products in Algolia. By default, it’s `products`. - -Next, change the content of `src/lib/search-client.ts` to the following: - -```ts title="src/lib/search-client.ts" -import algoliasearch from "algoliasearch/lite" - -const appId = process.env.NEXT_PUBLIC_SEARCH_APP_ID || "" - -const apiKey = process.env.NEXT_PUBLIC_SEARCH_API_KEY || "" - -export const searchClient = algoliasearch(appId, apiKey) - -export const SEARCH_INDEX_NAME = - process.env.NEXT_PUBLIC_INDEX_NAME || "products" -``` - -And change the content of `src/app/(main)/search/actions.ts` to the following: - -```ts title="src/app/(main)/search/actions.ts" -"use server" - -import { - searchClient, - SEARCH_INDEX_NAME, -} from "@lib/search-client" - -/** - * Uses MeiliSearch or Algolia to search for a query - * @param {string} query - search query - */ -export async function search(query: string) { - const index = searchClient.initIndex(SEARCH_INDEX_NAME) - const { hits } = await index.search(query) - - return hits -} -``` - -Then, restart the Next.js Starter storefront. You can search through available products by clicking the search icon in the navigation bar. - - - -To make sure the Next.js Starter storefront properly displays the products in the search result, include in the `attributesToRetrieve` setting of the Algolia plugin on the Medusa application at least the fields `title`, `handle`, `description`, and `thumbnail`. - - - --- ## Change Medusa Application URL diff --git a/www/apps/resources/app/recipes/b2b/page.mdx b/www/apps/resources/app/recipes/b2b/page.mdx index 315c469908..13fd21720a 100644 --- a/www/apps/resources/app/recipes/b2b/page.mdx +++ b/www/apps/resources/app/recipes/b2b/page.mdx @@ -8,6 +8,12 @@ export const metadata = { This recipe provides the general steps to implement a B2B store with Medusa. + + +This recipe is a work in progress, as some features are not ready yet in Medusa V2. + + + ## Overview In a B2B store, you provide different types of customers with relevant pricing, products, shopping experience, and more. @@ -180,6 +186,12 @@ This is useful in B2B sales, as you often negotiate special prices with each cus You can create a B2B module that adds necessary data models to represent a B2B company. Then, you link that company to a customer group. Any customer belonging to that group also belongs to the company, meaning they're a B2B customer. + + +Module Relationships is coming soon. + + + -} showLinkIcon={false} className="mt-1" -/> +/> */} -
+{/*
In this section, you'll create a B2B module that has a `Company` data model. The `Company` data model has a relationship to the `CustomerGroup` data model of the Customer Module. Start by creating the `src/modules/b2b` directory. Then, create the file `src/modules/b2b/models/company.ts` with the following content: - ```ts title="src/modules/b2b/models/company.ts" highlights={[["23", "", "Field will be used to create a relationship to customer groups."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" - import { BaseEntity } from "@medusajs/utils" - import { - Entity, - PrimaryKey, - Property, - } from "@mikro-orm/core" + ```ts title="src/modules/b2b/models/company.ts" highlights={[["8", "", "The property will be used to create a relationship to customer groups."]]} + import { model } from "@medusajs/utils" - @Entity() - export class Company extends BaseEntity { - @PrimaryKey({ columnType: "text" }) - id!: string + const Company = model.define("company", { + id: model.id(), + name: model.text(), + city: model.text(), + country_code: model.text(), + customer_group_id: model.text().nullable(), + }) - @Property({ columnType: "text" }) - name: string - - @Property({ columnType: "text" }) - city: string - - @Property({ columnType: "text" }) - country_code: string - - @Property({ columnType: "text" }) - customer_group_id?: string - } + export default Company ``` - This creates a `Company` data model with some relevant fields. Most importantly, it has a `customer_group_id` field. It'll later be used when creating the relationship to the `CustomerGroup` data model in the Customer Module. + This creates a `Company` data model with some relevant properties. Most importantly, it has a `customer_group_id` property. It'll later be used when creating the relationship to the `CustomerGroup` data model in the Customer Module. Next, create the migration in the file `src/modules/b2b/migrations/Migration20240516081502.ts` with the following content: @@ -261,106 +260,22 @@ You can create a B2B module that adds necessary data models to represent a B2B c You'll run the migration to reflect the data model in the database after finishing the module definition. - Before creating the module's main service, create the file `src/types/b2b/index.ts` with some helper types: - - ```ts title="src/types/b2b/index.ts" - import { CustomerGroupDTO } from "@medusajs/types" - - export type CompanyDTO = { - id: string - name: string - city: string - country_code: string - customer_group_id?: string - customer_group?: CustomerGroupDTO - } - - export type CreateCompanyDTO = { - name: string - city: string - country_code: string - customer_group_id?: string - } - - ``` - - You can now create the module's main service at `src/modules/b2b/service.ts` with the following content: - -export const mainServiceHighlights = [ - ["42", "relationships", "Implement the relationship to the `CustomerGroup` data model in the Customer Module."], - ["56", "create", "Implement a create method to create a company."] -] + Then, create the module's main service at `src/modules/b2b/service.ts` with the following content: - ```ts title="src/modules/b2b/service.ts" highlights={mainServiceHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" - import { ModulesSdkUtils } from "@medusajs/utils" - import { ModuleJoinerConfig, ModulesSdkTypes } from "@medusajs/types" - import { Modules } from "@medusajs/modules-sdk" - import { Company } from "./models/company" - import { CompanyDTO, CreateCompanyDTO } from "../../types/b2b" - - type InjectedDependencies = { - companyService: ModulesSdkTypes.InternalModuleService + ```ts title="src/modules/b2b/service.ts" + import { MedusaService } from "@medusajs/utils" + import Company from "./models/company" + + class B2bModuleService extends MedusaService({ + Company, + }){ + // TODO add custom methods } - type AllModelsDTO = { - Company: { - dto: CompanyDTO - } - } - - class B2bModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - InjectedDependencies, - CompanyDTO, - AllModelsDTO - >(Company, []) { - companyService_: ModulesSdkTypes.InternalModuleService - - constructor({ companyService }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - this.companyService_ = companyService - } - - __joinerConfig(): ModuleJoinerConfig { - return { - serviceName: "b2bModuleService", - alias: [ - { - name: ["company"], - args: { - entity: Company.name, - }, - }, - ], - relationships: [ - { - serviceName: Modules.CUSTOMER, - alias: "customer_group", - primaryKey: "id", - foreignKey: "customer_group_id", - args: { - methodSuffix: "CustomerGroups", - }, - }, - ], - } - } - - async create(data: CreateCompanyDTO): Promise { - const company = this.companyService_.create(data) - - return company - } - } - export default B2bModuleService ``` - This creates a `B2bModuleService` that extends the service factory and implements: - - - The module's relationship to the `CustomerGroup` data model in the Customer Module within the `__joinerConfig` method. - - A `create` method to create a company. + This creates a `B2bModuleService` that extends the service factory, which generates data-management functionalities for the `Company` data model. Next, create the module definition at `src/modules/b2b/index.ts` with the following content: @@ -398,7 +313,30 @@ export const mainServiceHighlights = [ To test out using the B2B Module, you'll add an API route to create a company. - Start by creating the file `src/workflows/create-company.ts` with the following content: + Start by creating the file `src/types/b2b/index.ts` with some helper types: + + ```ts title="src/types/b2b/index.ts" + import { CustomerGroupDTO } from "@medusajs/types" + + export type CompanyDTO = { + id: string + name: string + city: string + country_code: string + customer_group_id?: string + customer_group?: CustomerGroupDTO + } + + export type CreateCompanyDTO = { + name: string + city: string + country_code: string + customer_group_id?: string + } + + ``` + + Then, create the file `src/workflows/create-company.ts` with the following content: export const workflowHighlights = [ ["23", "tryToCreateCustomerGroupStep", "This step creates the customer group if its data is passed in the `customer_group` property."], @@ -419,7 +357,7 @@ export const workflowHighlights = [ import { CreateCustomerGroupDTO } from "@medusajs/types" import { CompanyDTO, CreateCompanyDTO } from "../types/b2b" import B2bModuleService from "../modules/b2b/service" - + export type CreateCompanyWorkflowInput = CreateCompanyDTO & { customer_group?: CreateCustomerGroupDTO } @@ -471,7 +409,7 @@ export const workflowHighlights = [ "b2bModuleService" ) - const company = await b2bModuleService.create( + const company = await b2bModuleService.createCompany( companyData ) @@ -498,10 +436,10 @@ export const workflowHighlights = [ You create a workflow with two steps: - 1. The first one tries to create a customer group if its data is provided in the `customer_group` property and sets its value in the `customer_group_id` field. + 1. The first one tries to create a customer group if its data is provided in the `customer_group` property and sets its value in the `customer_group_id` property. 2. The second one creates the company. - Then, create the file `src/api/admin/b2b/company/route.ts` with the following content: + Finally, create the file `src/api/admin/b2b/company/route.ts` with the following content: ```ts title="src/api/admin/b2b/company/route.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports" import type { @@ -566,7 +504,7 @@ export const workflowHighlights = [ -
+
*/} ## Add B2B Customers @@ -687,18 +625,21 @@ The API route can check if the customer has any group with an associated company showLinkIcon={false} /> -
+{/*
For example, create the API route `src/api/store/b2b/check-customer/route.ts` with the following content: export const checkCustomerHighlights = [ - ["16", "retrieve", "Retrieve the customer along with its groups."], - ["20", "list", "List the companies that have a customer group ID matching any of the customer's group IDs."], - ["25", "", "Return whether there are any companies associated with the customer's groups."] + ["19", "retrieveCustomer", "Retrieve the customer along with its groups."], + ["26", "listCompanies", "List the companies that have a customer group ID matching any of the customer's group IDs."], + ["31", "", "Return whether there are any companies associated with the customer's groups."] ] ```ts title="src/api/store/b2b/check-customer/route.ts" highlights={checkCustomerHighlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" - import type { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/medusa" + import type { + AuthenticatedMedusaRequest, + MedusaResponse, + } from "@medusajs/medusa" import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { ICustomerModuleService } from "@medusajs/types" import B2bModuleService from "../../../../modules/b2b/service" @@ -713,11 +654,14 @@ export const checkCustomerHighlights = [ "b2bModuleService" ) - const customer = await customerModuleService.retrieve(req.auth.actor_id, { - relations: ["groups"], - }) + const customer = await customerModuleService.retrieveCustomer( + req.auth_context.actor_id, + { + relations: ["groups"], + } + ) - const companies = await b2bModuleService.list({ + const companies = await b2bModuleService.listCompanies({ customer_group_id: customer.groups.map((group) => group.id), }) @@ -736,7 +680,10 @@ export const checkCustomerHighlights = [ Before using the API route, create the file `src/api/middlewares.ts` with the following content: ```ts title="src/api/middlewares.ts" - import { MiddlewaresConfig, authenticate } from "@medusajs/medusa" + import { + MiddlewaresConfig, + authenticate, + } from "@medusajs/medusa" export const config: MiddlewaresConfig = { routes: [ @@ -796,16 +743,12 @@ export const checkCustomerHighlights = [ } ``` -
+
*/} + +--- ## Customize Admin - - -Admin customizations are coming soon! - - - Based on your use case, you may need to customize the Medusa Admin to add new widgets or pages. The Medusa Admin plugin can be extended to add widgets, new pages, and setting pages. @@ -851,9 +794,9 @@ Use the publishable API key you associated with your B2B sales channel in the st showLinkIcon: false }, { - href: "!docs!/storefront-development/tips", - title: "Storefront Tips", - text: "Find tips on developing a custom storefront.", + href: "/storefront-development", + title: "Storefront Development", + text: "Find guides for your storefront development.", startIcon: , showLinkIcon: false }, diff --git a/www/apps/resources/app/recipes/commerce-automation/page.mdx b/www/apps/resources/app/recipes/commerce-automation/page.mdx index 676793ac58..9bdb46c1f6 100644 --- a/www/apps/resources/app/recipes/commerce-automation/page.mdx +++ b/www/apps/resources/app/recipes/commerce-automation/page.mdx @@ -9,6 +9,12 @@ export const metadata = { This recipe provides the general steps to implement a B2B store with Medusa. + + +This recipe is a work in progress, as some features are not ready yet in Medusa V2. + + + ## Overview Commerce automation is essential for businesses to save costs, provide a better user experience, and avoid manual, repetitive tasks that lead to human errors. Businesses utilize automation in different domains, including marketing, customer support, and order management. @@ -49,13 +55,6 @@ The `inventory-item.updated` event is currently not emitted. startIcon: , showLinkIcon: false }, - { - href: "!docs!/advanced-development/modules/module-relationships", - title: "Module Relationships", - text: "Learn how to create module relationships.", - startIcon: , - showLinkIcon: false - }, ]} /> + class RestockNotificationModuleService extends MedusaService({ + RestockNotification, + }){ + // TODO add custom methods } - type AllModelDTOs = { - RestockNotification: { - dto: RestockNotificationDTO - } - } - - class RestockNotificationModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - InjectedDependencies, - RestockNotificationDTO, - AllModelDTOs - >(RestockNotification, []) { - restockNotificationService_: ModulesSdkTypes.InternalModuleService - - constructor({ restockNotificationService }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - this.restockNotificationService_ = restockNotificationService - } - - __joinerConfig(): ModuleJoinerConfig { - return { - serviceName: "restockNotificationModuleService", - alias: [ - { - name: "restock_notification", - args: { - entity: RestockNotification.name, - }, - }, - ], - relationships: [ - { - serviceName: Modules.PRODUCT, - alias: "variant", - primaryKey: "id", - foreignKey: "variant_id", - args: { - methodSuffix: "Variants", - }, - }, - { - serviceName: Modules.SALES_CHANNEL, - alias: "sales_channel", - primaryKey: "id", - foreignKey: "sales_channel_id", - }, - ], - } - } - - async create(data: CreateRestockNotificationDTO): Promise { - const restockNotification = await this.restockNotificationService_.create( - data - ) - - return restockNotification - } - } - export default RestockNotificationModuleService ``` - In the module's main service, you: - - - Extend the service factory to have basic data management features. - - Define a relationship to the `ProductVariant` data model of the Product Module. - - Define a relationship to the `SalesChannel` data model of the Sales Channel Module. - - Implement the `create` method to create a restock notification. + The module's main service extends the service factory which generates basic management features for the `RestockNotification` data model. Next, create the module's definition file `src/modules/restock-notification/index.ts` with the following content: @@ -290,18 +185,19 @@ export const restockModuleService = [ Create the file `src/api/store/restock-notification/route.ts` with the following content: - ```ts title="src/api/store/restock-notification/route.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports" + ```ts title="src/api/store/restock-notification/route.ts" collapsibleLines="1-13" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/medusa" import RestockNotificationModuleService from "../../../modules/restock-notification/service" - import { - CreateRestockNotificationDTO, - } from "../../../types/restock-notification" - - type RestockNotificationReq = CreateRestockNotificationDTO + + type RestockNotificationReq = { + email: string + variant_id: string + sales_channel_id: string + } export async function POST( req: MedusaRequest, @@ -312,7 +208,7 @@ export const restockModuleService = [ "restockNotificationModuleService" ) - await restockNotificationModuleService.create( + await restockNotificationModuleService.createRestockNotifications( req.body ) @@ -326,13 +222,15 @@ export const restockModuleService = [ ### Create Inventory Item Updated Subscriber + To handle the sending of the restock notifications, create a subscriber that listens to the `inventory-item.updated` event, then sends a notification using the Notification Module to subscribed emails. + - The `inventory-item.updated` event is currently not emitted. The subscriber will only work once the event is emitted. + The `inventory-item.updated` event is currently not emitted. - To handle the sending of the restock notifications, create the file `src/subscribers/inventory-item-update.ts` with the following content: + {/* To handle the sending of the restock notifications, create the file `src/subscribers/inventory-item-update.ts` with the following content: */} export const subscriberHighlights = [ ["48", "inventoryVariantLinkService", "Retrieve an instance of the link service for the product-variant-inventory-item link module."], @@ -343,19 +241,19 @@ export const subscriberHighlights = [ ["93", "salesChannelLocations", "Retrieve the stock locations linked to the restock notification's sales channel."], ["107", "availableQuantity", "Retrieve the available quantity of the variant in the retrieved stock locations."], ["116", "continue", "Only send the notification if the available quantity is greater than `0`"], - ["119", "create", "Send the notification to the customer using the Notification Module."], + ["119", "createNotifications", "Send the notification to the customer using the Notification Module."], ["122", '"test_template"', "Replace with the actual template used for sending the email."], ["123", "data", "The data to send along to the third-party service sending the notification."], - ["131", "delete", "Delete the restock notification to not send the notification again."] + ["131", "deleteRestockNotifications", "Delete the restock notification to not send the notification again."] ] - ```ts title="src/subscribers/inventory-item-update.ts" highlights={subscriberHighlights} collapsibleLines="1-23" expandButtonLabel="Show Imports" + {/* ```ts title="src/subscribers/inventory-item-update.ts" highlights={subscriberHighlights} collapsibleLines="1-20" expandButtonLabel="Show Imports" import type { SubscriberArgs, SubscriberConfig, } from "@medusajs/medusa" import { - IInventoryServiceNext, + IInventoryService, INotificationModuleService, RemoteQueryFunction, } from "@medusajs/types" @@ -364,9 +262,6 @@ export const subscriberHighlights = [ Modules, remoteQueryObjectFromString, } from "@medusajs/utils" - import { - RestockNotificationDTO, - } from "../types/restock-notification" import { RemoteLink, } from "@medusajs/modules-sdk" @@ -388,7 +283,7 @@ export const subscriberHighlights = [ RestockNotificationModuleService = container.resolve( "restockNotificationModuleService" ) - const inventoryModuleService: IInventoryServiceNext = + const inventoryModuleService: IInventoryService = container.resolve(Modules.INVENTORY) const notificationModuleService: INotificationModuleService = container.resolve( @@ -430,7 +325,7 @@ export const subscriberHighlights = [ }, }) - const restockNotifications: RestockNotificationDTO[] = + const restockNotifications = await remoteQuery(restockQuery) const salesChannelLocationService = remoteLink.getLinkModule( @@ -468,7 +363,7 @@ export const subscriberHighlights = [ continue } - notificationModuleService.create({ + notificationModuleService.createNotifications({ to: restockNotification.email, channel: "email", template: "test_template", @@ -480,7 +375,8 @@ export const subscriberHighlights = [ }) // delete the restock notification - await restockNotificationModuleService.delete(restockNotification.id) + await restockNotificationModuleService + .deleteRestockNotifications(restockNotification.id) } } @@ -497,7 +393,7 @@ export const subscriberHighlights = [ - Retrieve the restock notifications of those variants. - For each restock notification, you: - Retrieve its quantity based on the stock location associated with the restock notification's sales channel. - - If the quantity is greater than `0`, you send a notification using the Notification Module and delete the restock notification. + - If the quantity is greater than `0`, you send a notification using the Notification Module and delete the restock notification. */} @@ -509,11 +405,11 @@ Customer support is essential to build a store's brand and customer loyalty. Thi You can use the Notification Module to send notifications when an action is triggered, such as when a customer or their order is updated. - +{/* The [Events reference](../../events-reference/page.mdx) shows an extensive list of events triggered by the each commerce module. - + */} Medusa also provides Notification Provider Modules that integrate with third-party services, such as SendGrid. @@ -564,28 +460,39 @@ Scheduled jobs are coming soon. For example, create the file `src/workflows/sync-products.ts` with the following content: export const syncProductsWorkflowHighlight = [ - ["9", "retrieveStoreStep", "A step that retrieves the store by its ID."], - ["25", "retrieveProductsToUpdateStep", "A step that retrieves the products to update based on a last update date."], - ["45", "syncProductsStep", "A step to sync the product with a third-party service."], - ["48", "productSyncModuleService", "Assuming this is a custom module's main service that provides connection to the third-party service."], - ["52", "productsBeforeSync", "Retrieve old product data from third-party service for compensation function."], - ["57", "sync", "Sync the product data in the third-party service."], - ["61", "", "Pass products data before sync to compensation function."], - ["64", "", "A compensation function to revert the sync when an error occurs."], - ["70", "sync", "Revert the product's data in the third-party service to its old data before the synchronization."], - ["79", "updateStoreLastSyncStep", "A step to update the `last_sync_data` of the store."], - ["85", "prevLastSyncDate", "Retrieve the previous value of `last_sync_date` to pass it to compensation function."], - ["87", "update", "Update the `last_sync_date` of the store."], - ["95", "", "Pass previous last sync date to compensation function."], - ["98", "", "A compensation function to revert the update of `last_sync_data` if an error occurs."], - ["114", "syncProductsWorkflow", "Define the workflow that uses the above steps."] + ["20", "retrieveStoreStep", "A step that retrieves the store by its ID."], + ["36", "retrieveProductsToUpdateStep", "A step that retrieves the products to update based on a last update date."], + ["56", "syncProductsStep", "A step to sync the product with a third-party service."], + ["59", "productSyncModuleService", "Assuming this is a custom module's main service that provides connection to the third-party service."], + ["63", "productsBeforeSync", "Retrieve old product data from third-party service for compensation function."], + ["68", "sync", "Sync the product data in the third-party service."], + ["72", "", "Pass products data before sync to compensation function."], + ["75", "", "A compensation function to revert the sync when an error occurs."], + ["81", "sync", "Revert the product's data in the third-party service to its old data before the synchronization."], + ["90", "updateStoreLastSyncStep", "A step to update the `last_sync_data` of the store."], + ["96", "prevLastSyncDate", "Retrieve the previous value of `last_sync_date` to pass it to compensation function."], + ["98", "update", "Update the `last_sync_date` of the store."], + ["106", "", "Pass previous last sync date to compensation function."], + ["109", "", "A compensation function to revert the update of `last_sync_data` if an error occurs."], + ["125", "syncProductsWorkflow", "Define the workflow that uses the above steps."] ] - ```ts title="src/workflows/sync-products.ts" highlights={syncProductsWorkflowHighlight} - import { ModuleRegistrationName } from "@medusajs/modules-sdk" - import { IProductModuleService, IStoreModuleService, ProductDTO, StoreDTO } from "@medusajs/types" - import { StepResponse, createStep, createWorkflow } from "@medusajs/workflows-sdk" - + ```ts title="src/workflows/sync-products.ts" highlights={syncProductsWorkflowHighlight} collapsibleLines="1-16" expandButtonLabel="Show Imports" + import { + ModuleRegistrationName + } from "@medusajs/modules-sdk" + import { + IProductModuleService, + IStoreModuleService, + ProductDTO, + StoreDTO + } from "@medusajs/types" + import { + StepResponse, + createStep, + createWorkflow + } from "@medusajs/workflows-sdk" + type RetrieveStoreStepInput = { id: string } @@ -596,7 +503,7 @@ export const syncProductsWorkflowHighlight = [ const storeModuleService: IStoreModuleService = container.resolve(ModuleRegistrationName.STORE) - const store = await storeModuleService.retrieve(id) + const store = await storeModuleService.retrieveStore(id) return new StepResponse({ store }) } @@ -612,7 +519,7 @@ export const syncProductsWorkflowHighlight = [ const productModuleService: IProductModuleService = container.resolve(ModuleRegistrationName.PRODUCT) - const products = await productModuleService.list({ + const products = await productModuleService.listProducts({ updated_at: { $gt: last_sync_date, }, @@ -633,7 +540,7 @@ export const syncProductsWorkflowHighlight = [ "productSyncModuleService" ) - const productsBeforeSync = await productSyncModuleService.list({ + const productsBeforeSync = await productSyncModuleService.listProductSyncs({ id: products.map((product) => product.id), }) @@ -668,7 +575,7 @@ export const syncProductsWorkflowHighlight = [ const prevLastSyncDate = store.metadata.last_sync_date - await storeModuleService.update(store.id, { + await storeModuleService.updateStores(store.id, { metadata: { last_sync_date: (new Date()).toString(), }, @@ -683,7 +590,7 @@ export const syncProductsWorkflowHighlight = [ const storeModuleService: IStoreModuleService = container.resolve(ModuleRegistrationName.STORE) - await storeModuleService.update(id, { + await storeModuleService.updateStores(id, { metadata: { last_sync_date, }, @@ -722,7 +629,7 @@ export const syncProductsWorkflowHighlight = [ This creates a workflow with the following steps: 1. Retrieve the store by its ID. - 2. Retrieve products to update based on the last date and time the products were synced. The last sync date is retrieved from the store's `metadata` field. + 2. Retrieve products to update based on the last date and time the products were synced. The last sync date is retrieved from the store's `metadata` property. 3. Sync the retrieved products with a third-party service. It's assumed that the connection to the third-party service is implemented within a custom module's main service. 4. Update the last sync date of the store to the current date. @@ -742,7 +649,15 @@ The `order.placed` event is currently not emitted. -} + showLinkIcon={false} +/> + +{/* , showLinkIcon: false }, -]} /> +]} /> */} --- @@ -786,13 +701,6 @@ Medusa's commerce features are geared towards automating RMA flows and ensuring startIcon: , showLinkIcon: false }, - { - href: "/events-reference", - title: "Events Reference", - text: "Check out triggered events by each commerce module.", - startIcon: , - showLinkIcon: false - }, ]} /> --- @@ -891,7 +799,7 @@ The `order.placed` event is currently not emitted. } // retrieve the order - const order = await orderModuleService.retrieve(orderId) + const order = await orderModuleService.retrieveOrder(orderId) if (!order || !order.customer_id || @@ -901,7 +809,7 @@ The `order.placed` event is currently not emitted. return } - const [, count] = await orderModuleService.listAndCount({ + const [, count] = await orderModuleService.listAndCountOrders({ customer_id: order.customer_id, }) @@ -948,13 +856,6 @@ Scheduled jobs are coming soon. startIcon: , showLinkIcon: false }, - { - href: "/events-reference", - title: "Events Reference", - text: "Check out triggered events in each commerce module.", - startIcon: , - showLinkIcon: false - }, { href: "!docs!/basics/scheduled-jobs", title: "Scheduled Jobs", @@ -969,18 +870,18 @@ Scheduled jobs are coming soon. For example, create the file `src/subscribers/send-products-newsletter.ts` with the following content: export const newsletterHighlights = [ - ["35", "store", "Retrieve the first store in the application."], - ["37", "products", "Retrieve the products created since the last newsletter send date."], - ["43", "", "Check whether more than 10 products have been created before proceeding."], - ["47", "customers", "Retrieve all customers, assuming they're considered subscribed."], - ["49", "create", "Send a notification (newsletter) to each customer using the Notification Module."], - ["52", '"email"', "Send the notification through the email channel."], - ["53", '"newsletter_template"', "Specify the template name in the third-party service (for example, SendGrid)."], - ["55", "products", "Pass the created products to the template."], - ["60", "update", "Update the store's `last_newsletter_send_date` field with the current date."] + ["33", "store", "Retrieve the first store in the application."], + ["35", "products", "Retrieve the products created since the last newsletter send date."], + ["41", "", "Check whether more than 10 products have been created before proceeding."], + ["45", "customers", "Retrieve all customers, assuming they're considered subscribed."], + ["47", "createNotifications", "Send a notification (newsletter) to each customer using the Notification Module."], + ["50", '"email"', "Send the notification through the email channel."], + ["51", '"newsletter_template"', "Specify the template name in the third-party service (for example, SendGrid)."], + ["53", "products", "Pass the created products to the template."], + ["58", "updateStores", "Update the store's `last_newsletter_send_date` property with the current date."] ] - ```ts title="src/subscribers/send-products-newsletter.ts" highlights={newsletterHighlights} collapsibleLines="1-16" expandButtonLabel="Show Imports" + ```ts title="src/subscribers/send-products-newsletter.ts" highlights={newsletterHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports" import type { SubscriberArgs, SubscriberConfig, @@ -988,13 +889,11 @@ export const newsletterHighlights = [ import { ModuleRegistrationName, } from "@medusajs/modules-sdk" - import { - NotificationModuleService, - } from "@medusajs/notification" import { ICustomerModuleService, IProductModuleService, IStoreModuleService, + INotificationModuleService, } from "@medusajs/types" export default async function productCreateHandler({ @@ -1011,13 +910,13 @@ export const newsletterHighlights = [ container.resolve(ModuleRegistrationName.CUSTOMER) const notificationModuleService: - NotificationModuleService = container.resolve( + INotificationModuleService = container.resolve( ModuleRegistrationName.NOTIFICATION ) - const store = (await storeModuleService.list())[0] + const store = (await storeModuleService.listStores())[0] - const products = await productModuleService.list({ + const products = await productModuleService.listProducts({ created_at: { $gt: store.metadata.last_newsletter_send_date, }, @@ -1027,9 +926,9 @@ export const newsletterHighlights = [ return } - const customers = await customerModuleService.list() + const customers = await customerModuleService.listCustomers() - await notificationModuleService.create( + await notificationModuleService.createNotifications( customers.map((customer) => ({ to: customer.email, channel: "email", @@ -1040,7 +939,7 @@ export const newsletterHighlights = [ })) ) - await storeModuleService.update(store.id, { + await storeModuleService.updateStores(store.id, { metadata: { last_newsletter_send_date: (new Date()).toString(), }, @@ -1055,7 +954,7 @@ export const newsletterHighlights = [ In the subscriber function, you: 1. Retrieve the first store in our application. - 2. Retrieve products created since the last time a newsletter is sent. The last send date is stored in the store's `metadata` field. + 2. Retrieve products created since the last time a newsletter is sent. The last send date is stored in the store's `metadata` property. 3. If the count of last created products is less than 10, stop execution. 4. Retrieve all customers. Here, it's assumed that all customers are considered subscribed for simplicity. 5. Use the Notification Module to send a notification to the customer. This uses the Notification Provider Module configured for the `email` channel. diff --git a/www/apps/resources/app/recipes/digital-products/page.mdx b/www/apps/resources/app/recipes/digital-products/page.mdx index 916efe2a4c..9226d5149b 100644 --- a/www/apps/resources/app/recipes/digital-products/page.mdx +++ b/www/apps/resources/app/recipes/digital-products/page.mdx @@ -9,6 +9,12 @@ export const metadata = { This recipe provides the general steps to implement digital products in your Medusa application. + + +This recipe is a work in progress, as some features are not ready yet in Medusa V2. + + + ## Overview Digital products are stored privately using a storage service like S3. When the customer buys this type of product, an email is sent to them where they can download the product. @@ -65,15 +71,11 @@ You can use one of Medusa’s notification provider modules or create your own. showLinkIcon: false }, { - href: "#", + href: "/references/notification-provider-module", title: "Create a Notification Provider Module", text: "Learn how to create a custom notification service.", startIcon: , showLinkIcon: false, - badge: { - variant: "blue", - children: "Guide Soon" - } }, ]} /> @@ -86,7 +88,7 @@ Your custom features and functionalities are implemented inside modules. The mod The module will hold your custom data models and the service implementing digital-product-related features. } @@ -144,17 +146,23 @@ The module will hold your custom data models and the service implementing digita A data model represents a table in the database. You can define in your module data models to store data related to your custom features. -To represent a digital product, it's recommended to create a data model that has a `variant_id` field. In a later section, you’ll learn how to add a relationship to the Product Module’s `ProductVariant` data model. +To represent a digital product, it's recommended to create a data model that has a `variant_id` property. In a later section, you’ll learn how to add a relationship to the Product Module’s `ProductVariant` data model. + + + +Module Relationships is coming soon. + + } showLinkIcon={false} /> -
+{/*
In this section, you’ll create a `ProductMedia` data model that represents your digital products. @@ -170,40 +178,22 @@ To represent a digital product, it's recommended to create a data model that has Then, create the file `src/modules/digital-product/models/product-media.ts` with the following content: ```ts title="src/modules/digital-product/models/product-media.ts" - import { Entity, Enum, PrimaryKey, Property } from "@mikro-orm/core" - import { createPsqlIndexStatementHelper, BaseEntity } from "@medusajs/utils" + import { model } from "@medusajs/utils" import { MediaType } from "../../../types/digital-product/product-media" - - const VariantIdIndex = createPsqlIndexStatementHelper({ - name: "IDX_product_media_variant_id", - tableName: "product_media", - columns: "variant_id", - }).MikroORMIndex - - @Entity({ tableName: "product_media" }) - export class ProductMedia extends BaseEntity { - @PrimaryKey({ columnType: "text" }) - id: string - - @Property({ columnType: "text" }) - name: string - - @Enum({ items: ["main", "preview"] }) - type: MediaType - - @Property({ columnType: "text" }) - file_key: string - - @Property({ columnType: "text" }) - mime_type: string - - @VariantIdIndex() - @Property({ columnType: "text" }) - variant_id: string - } + + const ProductMedia = model.define("product_media", { + id: model.id(), + name: model.text(), + type: model.enum(Object.values(MediaType)), + fileKey: model.text(), + mimeType: model.text(), + variant_id: model.text().index("IDX_product_media_variant_id"), + }) + + export default ProductMedia ``` - The `ProductMedia` data model has fields relevant to digital products. Most importantly, it has a `variant_id` field that will later be used for its relationship with the Product Module. + The `ProductMedia` data model has properties relevant to digital products. Most importantly, it has a `variant_id` property that will later be used for its relationship with the Product Module. To reflect the data model in the database, you must create a migration. @@ -238,7 +228,7 @@ To represent a digital product, it's recommended to create a data model that has npx medusa migrations run ``` -
+
*/} --- @@ -248,150 +238,46 @@ Your module’s main service holds the management and other related features. Th Medusa facilitates implementing data-management features by providing a service factory. This service factory implements basic data-management features, so you only need to implement features specific to your module. -, - showLinkIcon: false - }, - { - href: "#", - title: "Implement Database Operations in a Service", - text: "Learn how to perform database operations like create a record in a service.", - startIcon: , - showLinkIcon: false - }, -]} /> +} + showLinkIcon={false} +/> -
+{/*
In this section, you’ll modify the `DigitalProductModuleService` you created earlier to provide data-management functionalities of the `ProductMedia` data model. - First, change the content of `src/types/digital-product/product-media.ts` to include more common types: + Change the content of `src/modules/digital-product/service.ts` to the following: - ```ts title="src/types/digital-product/product-media.ts" - import { ProductVariantDTO } from "@medusajs/types" - - export enum MediaType { - MAIN = "main", - PREVIEW = "preview" - } - - export type ProductMediaDTO = { - id: string - name: string - type: MediaType - file_key: string - mime_type: string - variant_id: string - variant?: ProductVariantDTO - } - - export type CreateProductMediaDTO = { - name: string - file_key: string - variant_id: string - type: string - mime_type: string - } - - export type UpdateProductMediaDTO = { - name?: string - file_key?: string - variant_id?: string - type?: string - mime_type?: string + ```ts title="src/modules/digital-product/service.ts" + import { MedusaService } from "@medusajs/utils" + import ProductMedia from "./models/product-media" + + class DigitalProductModuleService extends MedusaService({ + ProductMedia, + }){ + // TODO add custom methods } - ``` - - Then, change the content of `src/modules/digital-product/service.ts` to the following: - - ```ts title="src/modules/digital-product/service.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" - import { ProductMedia } from "./models/product-media" - import { ModulesSdkUtils } from "@medusajs/utils" - import { ModulesSdkTypes } from "@medusajs/types" - import { - CreateProductMediaDTO, - ProductMediaDTO, - UpdateProductMediaDTO, - } from "../../types/digital-product/product-media" - - type InjectedDependencies = { - productMediaService: ModulesSdkTypes.InternalModuleService< - any - > - } - - class DigitalProductModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - InjectedDependencies, - ProductMediaDTO, - { - ProductMedia: { - dto: ProductMediaDTO - } - } - >(ProductMedia, [], {}) { - protected readonly productMediaService_: - ModulesSdkTypes.InternalModuleService - - constructor( - { - productMediaService, - }: InjectedDependencies - ) { - // @ts-ignore - super(...arguments) - - this.productMediaService_ = productMediaService - } - - async create( - data: CreateProductMediaDTO - ): Promise { - const productMedia = await this.productMediaService_ - .create( - data - ) - - return productMedia - } - - async update( - id: string, - data: UpdateProductMediaDTO - ): Promise { - const productMedia = await this.productMediaService_ - .update({ - ...data, - id, - }) - - return productMedia - } - } - export default DigitalProductModuleService ``` - The `DigitalProductModuleService` now extends the generated service returned by the service factory. The generated service already has basic features implemented, such as listing, retrieving, and deleting records. + The `DigitalProductModuleService` now extends the service factory which generates data-management methods for the `ProductMedia` data model. - You implement in your service the `create` and `update` methods, which aren’t generated automatically. In them, use the `productMediaService` generated for the `ProductMedia` data model in your module's container. It has methods to perform database operations on the data model. +
*/} -
+{/* --- */} ---- - -## Add Relationship to Product Variants +{/* ## Add Relationship to Product Variants As mentioned in a previous section, the product media has a `variant_id` that points to the saleable product variant. The Product Module implements product variants. However, modules are isolated. So, to reference data models and records from other modules, you can use module relationships. -The Medusa application resolves module relationships without creating an actual dependency between the modules. This allows you to associate more fields with existing modules while maintaining module isolation. +The Medusa application resolves module relationships without creating an actual dependency between the modules. This allows you to associate more properties with existing modules while maintaining module isolation. @@ -474,7 +360,7 @@ The Medusa application resolves module relationships without creating an actual }) ``` - + */} --- @@ -486,16 +372,22 @@ You can create custom API routes that allow merchants to list and create digital To utilize the relationship to the Product Module, you use the remote query to fetch data across modules. + + +Fetching data across modules using the remote query is coming soon. + + + , showLinkIcon: false }, { - href: "#", + href: "!docs!/advanced-development/modules/remote-query", title: "Remote Query", text: "Learn about what the remote query is and how to use it.", startIcon: , @@ -503,7 +395,7 @@ To utilize the relationship to the Product Module, you use the remote query to f }, ]} /> -
+{/*
In this section, you’ll create a List and Create API routes to retrieve and create digital products. @@ -544,22 +436,6 @@ To utilize the relationship to the Product Module, you use the remote query to f variant_id?: string } - export type UpdateProductMediaDTO = { - name?: string - file_key?: string - variant_id?: string - type?: MediaType - mime_type?: string - } - - export type FilterableProductMediaProps = { - name?: string[] - file_key?: string[] - variant_id?: string[] - type?: MediaType[] - mime_type?: string[] - } - export type CreateProductMediaWorkflowInput = CreateProductMediaDTO & { product?: CreateProductWorkflowInputDTO @@ -569,7 +445,7 @@ To utilize the relationship to the Product Module, you use the remote query to f Then, create the file `src/workflows/digital-product/create.ts` with the following content: - ```ts title="src/workflows/digital-product/create.ts" collapsibleLines="1-20" expandButtonLabel="Show Imports" + ```ts title="src/workflows/digital-product/create.ts" collapsibleLines="1-19" expandButtonLabel="Show Imports" import { createWorkflow, WorkflowData, @@ -589,7 +465,7 @@ To utilize the relationship to the Product Module, you use the remote query to f ContainerRegistrationKeys, remoteQueryObjectFromString, } from "@medusajs/utils" - + const tryToCreateProductVariantStep = createStep( "try-to-create-product-variant-step", async (input: CreateProductMediaWorkflowInput, { container }) => { @@ -601,20 +477,20 @@ To utilize the relationship to the Product Module, you use the remote query to f }, throwOnError: false, }) - + if (errors.length) { throw errors[0].error } - + input.variant_id = result[0].variants[0].id - + delete input.product } - + return new StepResponse(input) } ) - + const createProductMediaStep = createStep( "create-product-media-step", async (input: CreateProductMediaDTO, { container }) => { @@ -622,22 +498,23 @@ To utilize the relationship to the Product Module, you use the remote query to f DigitalProductModuleService = container.resolve( "digitalProductModuleService" ) - - const productMedia = await digitalProductModuleService.create( - input - ) - + + const productMedia = await digitalProductModuleService + .createProductMedias( + input + ) + return new StepResponse(productMedia) } ) - + const retrieveProductMediaWithVariant = createStep( "retrieve-product-media-with-variant-step", async (input: ProductMediaDTO, { container }) => { const remoteQuery: RemoteQueryFunction = container.resolve( ContainerRegistrationKeys.REMOTE_QUERY ) - + const query = remoteQueryObjectFromString({ entryPoint: "product_media", fields: [ @@ -654,17 +531,17 @@ To utilize the relationship to the Product Module, you use the remote query to f }, }, }) - + const result = await remoteQuery(query) - + return new StepResponse(result[0]) } ) - + type WorkflowInput = { data: CreateProductMediaWorkflowInput } - + export const createProductMediaWorkflow = createWorkflow( "create-product-media-workflow", function (input: WorkflowData) { @@ -673,7 +550,7 @@ To utilize the relationship to the Product Module, you use the remote query to f const normalizedInput = tryToCreateProductVariantStep(input.data) const productMedia = createProductMediaStep(normalizedInput) - + return retrieveProductMediaWithVariant(productMedia) } ) @@ -681,7 +558,7 @@ To utilize the relationship to the Product Module, you use the remote query to f This workflow has three steps: - 1. If a `variant_id` field isn’t passed and a `product` field is passed, create the product using Medusa’s `createProductsWorkflow` and set the ID of the variant in the `variant_id` field. + 1. If a `variant_id` field isn’t passed and a `product` field is passed, create the product using Medusa’s `createProductsWorkflow` and set the ID of the variant in the `variant_id` property. 2. Use the `DigitalProductModuleService` to create the product media. 3. Use the remote query to retrieve the product media along with the variant it references. @@ -853,7 +730,7 @@ To utilize the relationship to the Product Module, you use the remote query to f You’ll receive a list of digital products. -
+
*/} --- @@ -866,14 +743,14 @@ In your customizations, you send requests to the API routes you created to creat , showLinkIcon: false }, { - href: "#", + href: "!docs!/advanced-development/admin/ui-routes", title: "Create UI Route", text: "Learn how to create a UI route in the Medusa Admin.", startIcon: , @@ -881,7 +758,7 @@ In your customizations, you send requests to the API routes you created to creat }, ]} /> -
+{/*
In this example, you’ll add a single page that lists the digital products and allows you to create a new one. The implementation will be minimal for the purpose of simplicity, so you can elaborate on it based on your use case. @@ -1172,7 +1049,7 @@ In your customizations, you send requests to the API routes you created to creat If you open the admin now, you’ll find a new Digital Products item in the sidebar. You can try adding Digital Products and viewing them. -
+
*/} --- @@ -1182,6 +1059,12 @@ When a customer purchases a digital product, they should receive a link to downl You can create a subscriber that listens to the `order.placed` event. In the subscriber, you check for the digital products in the order and obtain the download URLs using the file provider module’s `getPresignedDownloadUrl` method. + + +The `order.placed` event isn't currently emitted. + + + Following this approach assumes the file provider module you're using handles creating secure pre-signed URLs with an expiration mechanism. Alternatively, create a token on purchase using the API Key Module and create an API route that validates that token. @@ -1198,7 +1081,7 @@ In the subscriber, you can send a notification, such as an email, to the custome showLinkIcon={false} /> -
+{/*
@@ -1244,7 +1127,7 @@ In the subscriber, you can send a notification, such as an email, to the custome const orderId = "data" in data ? data.data.id : data.id - const order = await orderModuleService.retrieve(orderId, { + const order = await orderModuleService.retrieveOrder(orderId, { relations: ["items"], }) @@ -1252,7 +1135,7 @@ In the subscriber, you can send a notification, such as an email, to the custome const urls = [] for (const item of order.items) { const productMedias = await digitalProductModuleService - .list({ + .listProductMedias({ variant_id: [item.variant_id], }) @@ -1260,7 +1143,7 @@ In the subscriber, you can send a notification, such as an email, to the custome productMedias.map(async (productMedia) => { // get the download URL from the file service - return (await fileModuleService.retrieve( + return (await fileModuleService.retrieveFile( productMedia.file_key )).url }) @@ -1269,7 +1152,7 @@ In the subscriber, you can send a notification, such as an email, to the custome urls.push(...downloadUrls) } - notificationModuleService.create({ + notificationModuleService.createNotifications({ to: order.email, template: "digital-download", channel: "email", @@ -1287,7 +1170,7 @@ In the subscriber, you can send a notification, such as an email, to the custome The `handleOrderPlaced` subscriber function retrieves the order, loops over its items to find digital products and retrieve their download links, then uses the installed notification provider module to send the email, passing the URLs as a data payload. You can customize the sent data based on your template and your use case. -
+
*/} --- @@ -1297,1406 +1180,12 @@ Customers use your storefront to browse your digital products and purchase them. Medusa provides a Next.js storefront with standard commerce features including listing products, placing orders, and managing accounts. You can customize the storefront and cater its functionalities to support digital products. -Alternatively, you can build the storefront with your preferred tech stack. This guide has tips on building storefronts. - -The rest of this section provides some guidelines on how to customize the Next.js storefront to support digital products. - - - -While our team ensures to maintain this section with the changes in the Next.js storefront, some changes may cause the code in this section to be outdated. If you encounter outdated code snippets, please submit [an issue](https://github.com/medusajs/medusa/issues/new?assignees=&labels=type:+docs&projects=&template=docs.yml). - - - -### Preview Digital Product - -On the product detail page, you can add a button that allows customers to download a preview of the digital product. - -To implement this, create a storefront API Route that allows you to fetch the digital product, then customize the Next.js storefront to show the preview button if a product is digital. - -
- - Start by creating a store API Route in your Medusa application that lists digital products. - - Create the file `src/api/store/digital-products/route.ts` with the following content: - - ```ts title="src/api/store/product-media/route.ts" badgeLabel="Medusa Application" collapsibleLines="1-13" expandButtonLabel="Show Imports" - import type { - MedusaRequest, - MedusaResponse, - } from "@medusajs/medusa" - import { - MediaType, - } from "../../../types/digital-product/product-media" - import { RemoteQueryFunction } from "@medusajs/modules-sdk" - import { - ContainerRegistrationKeys, - remoteQueryObjectFromString, - } from "@medusajs/utils" - - export const GET = async ( - req: MedusaRequest, - res: MedusaResponse - ) => { - const remoteQuery: RemoteQueryFunction = req.scope.resolve( - ContainerRegistrationKeys.REMOTE_QUERY - ) - const query = remoteQueryObjectFromString({ - entryPoint: "product_media", - fields: [ - "id", - "name", - "file_key", - "mime_type", - "variant.*", - ], - variables: { - filters: { - type: MediaType.PREVIEW, - }, - // omitting pagination for simplicity - skip: 0, - }, - }) - - const { - rows, - metadata: { count }, - } = await remoteQuery(query) - - res.json({ - product_medias: rows, - count, - }) - } - ``` - - This adds a `GET` API route at `/store/product-media` that retrieves product media of type `preview` with their variant. - - {/* TODO check rest of steps */} - - Now, you can customize the Next.js Starter to show the preview button. - - First, if you're using TypeScript for your development, create the file `src/types/product-media.ts` with the following content: - - ```ts title="src/types/product-media.ts" badgeLabel="Storefront" badgeColor="blue" - import { Product } from "@medusajs/medusa" - import { ProductVariant } from "@medusajs/product" - - export enum ProductMediaVariantType { - PREVIEW = "preview", - MAIN = "main", - } - - export type ProductMedia = { - id: string - variant_id: string - name?: string - file_key?: string - mime_type?: string - created_at?: Date - updated_at?: Date - type?: ProductMediaVariantType - variants?: ProductVariant[] - } - - export type DigitalProduct = Omit & { - product_medias?: ProductMedia[] - variants?: DigitalProductVariant[] - } - - export type DigitalProductVariant = ProductVariant & { - product_medias?: ProductMedia - } - - ``` - - Then, add in `src/lib/data/index.ts` a new function that retrieves the product media of the product variant being viewed: - - ```ts title="src/lib/data/index.ts" badgeLabel="Storefront" badgeColor="blue" collapsibleLines="1-7" expandButtonLabel="Show Imports" - import { - ProductMedia, - } from "types/product-media" - import { Variant } from "../../types/medusa" - - // ... rest of the functions - - export async function getProductMediaPreviewByVariant( - variant: Variant - ): Promise { - const { - product_medias, - } = await medusaRequest( - "GET", - `/product-media`, - { - query: { - variant_ids: variant.id, - }, - } - ) - .then((res) => res.body) - .catch((err) => { - throw err - }) - - return product_medias[0] - } - ``` - - To allow customers to download the file preview without exposing its URL, create a Next.js API route in the file `src/app/api/download/preview/route.ts` with the following content: - - ```ts title="src/app/api/download/preview/route.ts" badgeLabel="Storefront" badgeColor="blue" - import { NextRequest, NextResponse } from "next/server" - - export async function GET(req: NextRequest) { - // Get the file info from the URL - const { - file_path, - file_name, - mime_type, - } = Object.fromEntries(req.nextUrl.searchParams) - - // Fetch the file - const fileResponse = await fetch(file_path) - - // Handle the case where the file could not be fetched - if (!fileResponse.ok) { - return new NextResponse("File not found", { status: 404 }) - } - - // Get the file content as a buffer - const fileBuffer = await fileResponse.arrayBuffer() - - // Define response headers - const headers = { - "Content-Type": mime_type, - // This sets the file name for the download - "Content-Disposition": `attachment; filename="${ - file_name - }"`, - } - - // Create a NextResponse with the file content and headers - const response = new NextResponse(fileBuffer, { - status: 200, - headers, - }) - - return response - } - ``` - - Next, create the preview button in the file `src/modules/products/components/product-media-preview/index.tsx`: - - ```tsx title="src/modules/products/components/product-media-preview/index.tsx" badgeLabel="Storefront" badgeColor="blue" - import Button from "@modules/common/components/button" - import { ProductMedia } from "types/product-media" - - type Props = { - media: ProductMedia - } - - const ProductMediaPreview: React.FC = ({ media }) => { - const downloadPreview = () => { - window.location.href = `${ - process.env.NEXT_PUBLIC_BASE_URL - }/api/download/preview?file_path=${ - media.file_key - }&file_name=${ - media.name - }&mime_type=${ - media.mime_type - }` - } - - return ( -
- -
- ) - } - - export default ProductMediaPreview - ``` - - Finally, add the button as one of the product actions defined in `src/modules/products/components/product-actions/index.tsx`. These are the actions shown to the customer in the product details page: - - ```tsx title="src/modules/products/components/product-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" collapsibleLines="1-6" expandButtonLabel="Show Imports" - // other imports... - import { useState, useEffect } from "react" - import ProductMediaPreview from "../product-media-preview" - import { getProductMediaPreviewByVariant } from "@lib/data" - import { ProductMedia } from "types/product-media" - - const ProductActions: React.FC = ({ - product, - }) => { - // other code... - - const [productMedia, setProductMedia] = useState< - ProductMedia - >() - - useEffect(() => { - const getProductMedia = async () => { - if (!variant) {return} - await getProductMediaPreviewByVariant(variant) - .then((res) => { - setProductMedia(res) - }) - } - getProductMedia() - }, [variant]) - - return ( -
- {/* other code... */} - - {productMedia && ( - - )} - - -
- ) - } - - export default ProductActions - ``` - -
- -### Update Product Tabs - -In the product details page, additional information related to the product and its shipping details are shown at the bottom right side. - -You can change this section to show information relevant to the product. For example, how many pages are in an e-book or how the e-book will be delivered to the customer. - -
- - {/* TODO check steps */} - - In this example, you'll change the content of the Product Information and Shipping & Returns tabs to show information relevant to the digital product. The Product Information tab will include custom information relevant to digital products, and the Shipping & Returns tab will be changed to "E-book delivery" and will hold details about how the e-book will be delivered to the customer. - - One way to store custom information relevant to the digital product is using the `metadata` field. For example, to store the number of pages of an e-book, set the `metadata` field to the following: - - ```json - { - "metadata": { - "Pages": "420" - } - } - ``` - - Then, you can customize the product additional details section to loop through the `metadata` field's properties and show their information. - - Next, change the `ProductTabs`, `ProductInfoTab`, and `ShippingInfoTab` components defined in `src/modules/products/components/product-tabs/index.tsx` to the following: - - ```tsx title="src/modules/products/components/product-tabs/index.tsx" badgeLabel="Storefront" badgeColor="blue" - const ProductTabs = ({ product }: ProductTabsProps) => { - const tabs = useMemo(() => { - return [ - { - label: "Product Information", - component: , - }, - { - label: "E-book delivery", - component: , - }, - ] - }, [product]) - // ... rest of code - } - - const ProductInfoTab = ({ product }: ProductTabsProps) => { - // map the metadata object to an array - const metadata = useMemo(() => { - if (!product.metadata) {return []} - return Object.keys(product.metadata).map((key) => { - return [key, product.metadata?.[key]] - }) - }, [product]) - - return ( - -
-
- {/* Map the metadata as product information */} - {metadata && - metadata.slice(0, 2).map(([key, value], i) => ( -
- {key} -

{value}

-
- ))} -
-
- {metadata.length > 2 && - metadata.slice(2, 4).map(([key, value], i) => { - return ( -
- {key} -

{value}

-
- ) - })} -
-
- {product.tags?.length ? ( -
- Tags -
- ) : null} -
- ) - } - - const ShippingInfoTab = () => { - return ( - -
-
- -
- - Instant delivery - -

- Your e-book will be delivered instantly via - email. You can also download it from your - account anytime. -

-
-
-
- -
- - Free previews - -

- Get a free preview of the e-book before - you buy it. Just click the - button above to download it. -

-
-
-
-
- ) - } - ``` - - This changes the titles of the tabs and their content. - -
- -### Change Shipping Form in Checkout - -When a customer purchases a digital product, the shipping form shown during checkout is irrelevant. So, you can change its content to instead only ask for the customer's name and email. - -
- - {/* TODO check steps */} - - The checkout flow is managed by a checkout context defined in `src/lib/context/checkout-context.tsx`. Change the content of the file to the following: - - ```tsx title="src/lib/context/checkout-context.tsx" badgeLabel="Storefront" badgeColor="blue" collapsibleLines="1-27" expandButtonLabel="Show Imports" - "use client" - - import { medusaClient } from "@lib/config" - import useToggleState, { StateType } from "@lib/hooks/use-toggle-state" - import { - Address, - Cart, - Customer, - StorePostCartsCartReq, - } from "@medusajs/medusa" - import Wrapper from "@modules/checkout/components/payment-wrapper" - import { isEqual } from "lodash" - import { - formatAmount, - useCart, - useCartShippingOptions, - useMeCustomer, - useRegions, - useSetPaymentSession, - useUpdateCart, - } from "medusa-react" - import { useRouter } from "next/navigation" - import React, { createContext, useContext, useEffect, useMemo } from "react" - import { FormProvider, useForm, useFormContext } from "react-hook-form" - import { useStore } from "./store-context" - import Spinner from "@modules/common/icons/spinner" - - type AddressValues = { - first_name: string - last_name: string - country_code: string - } - - export type CheckoutFormValues = { - shipping_address: AddressValues - billing_address: AddressValues - email: string - } - - interface CheckoutContext { - cart?: Omit - shippingMethods: { label?: string; value?: string; price: string }[] - isLoading: boolean - addressReady: boolean - shippingReady: boolean - paymentReady: boolean - readyToComplete: boolean - sameAsBilling: StateType - editAddresses: StateType - editShipping: StateType - editPayment: StateType - isCompleting: StateType - initPayment: () => Promise - setAddresses: (addresses: CheckoutFormValues) => void - setSavedAddress: (address: Address) => void - setShippingOption: (soId: string) => void - setPaymentSession: (providerId: string) => void - onPaymentCompleted: () => void - } - - const CheckoutContext = createContext(null) - - interface CheckoutProviderProps { - children?: React.ReactNode - } - - const IDEMPOTENCY_KEY = "create_payment_session_key" - - export const CheckoutProvider = ({ children }: CheckoutProviderProps) => { - const { - cart, - setCart, - addShippingMethod: { - mutate: setShippingMethod, - isLoading: addingShippingMethod, - }, - completeCheckout: { mutate: complete }, - } = useCart() - - const { customer } = useMeCustomer() - const { countryCode } = useStore() - - const methods = useForm({ - defaultValues: mapFormValues(customer, cart, countryCode), - reValidateMode: "onChange", - }) - - const { - mutate: setPaymentSessionMutation, - isLoading: settingPaymentSession, - } = useSetPaymentSession(cart?.id!) - - const { mutate: updateCart, isLoading: updatingCart } = useUpdateCart( - cart?.id! - ) - - const { shipping_options } = useCartShippingOptions(cart?.id!, { - enabled: !!cart?.id, - }) - - const { regions } = useRegions() - - const { resetCart, setRegion } = useStore() - const { push } = useRouter() - - const editAddresses = useToggleState() - const sameAsBilling = useToggleState( - cart?.billing_address && cart?.shipping_address - ? isEqual(cart.billing_address, cart.shipping_address) - : true - ) - - const editShipping = useToggleState() - const editPayment = useToggleState() - - /** - * Boolean that indicates if a part of the checkout is loading. - */ - const isLoading = useMemo(() => { - return addingShippingMethod || settingPaymentSession || updatingCart - }, [addingShippingMethod, settingPaymentSession, updatingCart]) - - /** - * Boolean that indicates if the checkout is ready to be completed. A checkout is ready to be completed if - * the user has supplied a email, shipping address, billing address, shipping method, and a method of payment. - */ - const { addressReady, shippingReady, paymentReady, readyToComplete } = - useMemo(() => { - const addressReady = - !!cart?.shipping_address && !!cart?.billing_address && !!cart?.email - - const shippingReady = - addressReady && - !!( - cart?.shipping_methods && - cart.shipping_methods.length > 0 && - cart.shipping_methods[0].shipping_option - ) - - const paymentReady = shippingReady && !!cart?.payment_session - - const readyToComplete = addressReady && shippingReady && paymentReady - - return { - addressReady, - shippingReady, - paymentReady, - readyToComplete, - } - }, [cart]) - - useEffect(() => { - if (addressReady && !shippingReady) { - editShipping.open() - } - }, [addressReady, shippingReady, editShipping]) - - const shippingMethods = useMemo(() => { - if (shipping_options && cart?.region) { - return shipping_options?.map((option) => ({ - value: option.id, - label: option.name, - price: formatAmount({ - amount: option.amount || 0, - region: cart.region, - }), - })) - } - - return [] - }, [shipping_options, cart]) - - /** - * Resets the form when the cart changed. - */ - useEffect(() => { - if (cart?.id) { - methods.reset(mapFormValues(customer, cart, countryCode)) - } - }, [customer, cart, methods, countryCode]) - - useEffect(() => { - if (!cart) { - editAddresses.open() - return - } - - if (cart?.shipping_address && cart?.billing_address) { - editAddresses.close() - return - } - - editAddresses.open() - }, [cart]) - - /** - * Method to set the selected shipping method for the cart. This is called when the user selects a shipping method, such as UPS, FedEx, etc. - */ - const setShippingOption = async (soId: string) => { - if (cart) { - setShippingMethod( - { option_id: soId }, - { - onSuccess: ({ cart }) => setCart(cart), - } - ) - } - } - - /** - * Method to create the payment sessions available for the cart. Uses a idempotency key to prevent duplicate requests. - */ - const initPayment = async () => { - if (cart?.id && !cart.payment_sessions?.length && cart?.items?.length) { - return medusaClient.carts - .createPaymentSessions(cart.id, { - "Idempotency-Key": IDEMPOTENCY_KEY, - }) - .then(({ cart }) => cart && setCart(cart)) - .catch((err) => err) - } - } - - useEffect(() => { - // initialize payment session - const start = async () => { - await initPayment() - } - start() - }, [cart?.region, cart?.id, cart?.items]) - - /** - * Method to set the selected payment session for the cart. This is called when the user selects a payment provider, such as Stripe, PayPal, etc. - */ - const setPaymentSession = (providerId: string) => { - if (cart) { - setPaymentSessionMutation( - { - provider_id: providerId, - }, - { - onSuccess: ({ cart }) => { - setCart(cart) - }, - } - ) - } - } - - const setSavedAddress = (address: Address) => { - const setValue = methods.setValue - - setValue("shipping_address", { - country_code: address.country_code || "", - first_name: address.first_name || "", - last_name: address.last_name || "", - }) - } - - /** - * Method that validates if the cart's region matches the shipping address's region. If not, it will update the cart region. - */ - const validateRegion = (countryCode: string) => { - if (regions && cart) { - const region = regions.find((r) => - r.countries.map((c) => c.iso_2).includes(countryCode) - ) - - if (region && region.id !== cart.region.id) { - setRegion(region.id, countryCode) - } - } - } - - /** - * Method that sets the addresses and email on the cart. - */ - const setAddresses = (data: CheckoutFormValues) => { - const { shipping_address, billing_address, email } = data - - validateRegion(shipping_address.country_code) - - const payload: StorePostCartsCartReq = { - shipping_address, - email, - } - - if (isEqual(shipping_address, billing_address)) { - sameAsBilling.open() - } - - if (sameAsBilling.state) { - payload.billing_address = shipping_address - } else { - payload.billing_address = billing_address - } - - updateCart(payload, { - onSuccess: ({ cart }) => setCart(cart), - }) - } - - const isCompleting = useToggleState() - - /** - * Method to complete the checkout process. This is called when the user clicks the "Complete Checkout" button. - */ - const onPaymentCompleted = () => { - isCompleting.open() - complete(undefined, { - onSuccess: ({ data }) => { - push(`/order/confirmed/${data.id}`) - resetCart() - }, - }) - isCompleting.close() - } - - return ( - - - {isLoading && cart?.id === "" ? ( -
-
- -
-
- ) : ( - {children} - )} -
-
- ) - } - - export const useCheckout = () => { - const context = useContext(CheckoutContext) - const form = useFormContext() - if (context === null) { - throw new Error( - "useProductActionContext must be used within a ProductActionProvider" - ) - } - return { ...context, ...form } - } - - /** - * Method to map the fields of a potential customer and - * the cart to the checkout form values. Information is - * assigned with the following priority: - * 1. Cart information - * 2. Customer information - * 3. Default values - null - */ - const mapFormValues = ( - customer?: Omit, - cart?: Omit, - currentCountry?: string - ): CheckoutFormValues => { - const customerShippingAddress = - customer?.shipping_addresses?.[0] - const customerBillingAddress = - customer?.billing_address - - return { - shipping_address: { - first_name: - cart?.shipping_address?.first_name || - customerShippingAddress?.first_name || - "", - last_name: - cart?.shipping_address?.last_name || - customerShippingAddress?.last_name || - "", - country_code: - currentCountry || - cart?.shipping_address?.country_code || - customerShippingAddress?.country_code || - "", - }, - billing_address: { - first_name: - cart?.billing_address?.first_name || - customerBillingAddress?.first_name || - "", - last_name: - cart?.billing_address?.last_name || - customerBillingAddress?.last_name || - "", - country_code: - cart?.shipping_address?.country_code || - customerBillingAddress?.country_code || - "", - }, - email: cart?.email || customer?.email || "", - } - } - ``` - - This removes all references to shipping fields that you don't need for digital products. - - Next, change the content of `src/modules/checkout/components/shipping-address/index.tsx` to remove the unnecessary address fields: - - ```tsx title="src/modules/checkout/components/shipping-address/index.tsx" badgeLabel="Storefront" badgeColor="blue" collapsibleLines="1-10" expandButtonLabel="Show Imports" - import { CheckoutFormValues } from "@lib/context/checkout-context" - import { emailRegex } from "@lib/util/regex" - import ConnectForm from "@modules/common/components/connect-form" - import Input from "@modules/common/components/input" - import { useMeCustomer } from "medusa-react" - import AddressSelect from "../address-select" - import CountrySelect from "../country-select" - import Checkbox from "@modules/common/components/checkbox" - import { Container } from "@medusajs/ui" - - const ShippingAddress = ({ - checked, - onChange, - }: { - checked: boolean - onChange: () => void - }) => { - const { customer } = useMeCustomer() - - return ( -
- {customer && (customer.shipping_addresses?.length || 0) > 0 && ( - -

- {`Hi ${customer.first_name}, do you want to use one of your saved addresses?`} -

- -
- )} - > - {({ register, formState: { errors, touchedFields } }) => ( - <> -
- - - -
-
- -
-
- -
- - )} - -
- ) - } - - export default ShippingAddress - ``` - - And update the content of `src/modules/checkout/components/addresses/index.tsx` to only show the necessary address fields: - - ```tsx title="src/modules/checkout/components/addresses/index.tsx" badgeLabel="Storefront" badgeColor="blue" - import { useCheckout } from "@lib/context/checkout-context" - import { Button } from "@medusajs/ui" - import Spinner from "@modules/common/icons/spinner" - import ShippingAddress from "../shipping-address" - - const Addresses = () => { - const { - sameAsBilling: { state: checked, toggle: onChange }, - editAddresses: { state: isOpen, open }, - editShipping: { close: closeShipping }, - editPayment: { close: closePayment }, - setAddresses, - handleSubmit, - cart, - } = useCheckout() - - const handleEdit = () => { - open() - closeShipping() - closePayment() - } - - return ( -
-
-
- 1 -
-

Shipping address

-
- {isOpen ? ( -
- - -
- ) : ( -
-
- {cart && cart.shipping_address ? ( -
-
- ✓ -
-
-
- - {cart.shipping_address.first_name}{" "} - {cart.shipping_address.last_name} - {cart.shipping_address.country} - -
- {cart.email} -
-
-
- -
-
-
- ) : ( -
- -
- )} -
-
- )} -
- ) - } - - export default Addresses - ``` - - Finally, change the shipping details shown in the order confirmation page by replacing the content of `src/modules/order/components/shipping-details/index.tsx` with the following: - - ```tsx title="src/modules/order/components/shipping-details/index.tsx" badgeLabel="Storefront" badgeColor="blue" - import { Order } from "@medusajs/medusa" - import { Heading, Text } from "@medusajs/ui" - import Divider from "@modules/common/components/divider" - import { formatAmount } from "medusa-react" - - type ShippingDetailsProps = { - order: Order - } - - const ShippingDetails = ({ order }: ShippingDetailsProps) => { - return ( -
- - Delivery - -
-
- - Shipping Address - - - {order.shipping_address.first_name}{" "} - {order.shipping_address.last_name} - - - {order.shipping_address.country_code?.toUpperCase()} - -
- -
- Contact - {order.email} -
- -
- Method - - {order.shipping_methods[0].shipping_option.name} ( - {formatAmount({ - amount: order.shipping_methods[0].price, - region: order.region, - }) - .replace(/,/g, "") - .replace(/\./g, ",")} - ) - -
-
- -
- ) - } - - export default ShippingDetails - ``` - -
- -### Download Product After Purchase - -After the customer purchases the digital product you can show a download button to allow them to immediately download the product. - -
- - Before you implement the storefront changes, you need to create a new API Route in the Medusa application that validates that the customer has already purchased the digital product before returning the presigned URL to download it. - - You’ll implement this logic in a workflow. Create the file `src/workflows/digital-product/get-url.ts` with the following content: - - ```ts title="src/api/store/product-media/download/route.ts" badgeLabel="Medusa Application" collapsibleLines="1-14" expandButtonLabel="Show Imports" - import { - createWorkflow, - transform, - createStep, - StepResponse, - } from "@medusajs/workflows-sdk" - import { - IOrderModuleService, - IFileModuleService, - } from "@medusajs/types" - import { ModuleRegistrationName } from "@medusajs/modules-sdk" - import DigitalProductModuleService from "../../modules/digital-product/service" - import { MediaType } from "../../types/digital-product/product-media" - - type GetPurchasedProductMediaUrlWorkflowInput = { - variant_id: string - customer_id: string - } - - type CheckVariantStepInput = - GetPurchasedProductMediaUrlWorkflowInput - - const checkVariantPurchasedStep = createStep( - "check-variant-purchased-step", - async ({ - variant_id, - customer_id, - }: CheckVariantStepInput, { - container, - }) => { - const orderModuleService: IOrderModuleService = - container.resolve(ModuleRegistrationName.ORDER) - const orders = await orderModuleService.list({ - customer_id, - }, { - relations: ["items"], - }) - - const found = orders.some((order) => ( - order.items.some((item) => { - if (item.variant_id === variant_id) { - return true - } - - return false - }) - )) - - if (!found) { - throw new Error("Customer hasn't purchased this product.") - } - } - ) - - type GetProductMediaStepInput = { - variant_id: string - } - - const getProductMediaStep = createStep( - "get-product-media-step", - async ({ variant_id }: GetProductMediaStepInput, { container }) => { - // get the product media and the presigned URL - const digitalProductModuleService: - DigitalProductModuleService = container.resolve( - "digitalProductModuleService" - ) - const productMedias = await digitalProductModuleService - .list({ - type: MediaType.MAIN, - variant_id, - }) - - return new StepResponse({ product_media: productMedias[0] }) - } - ) - - type GetFileUrlStepInput = { - id: string - } - - const getFileUrlStep = createStep( - "get-file-url-step", - async ({ id }: GetFileUrlStepInput, { container }) => { - const fileModuleService: IFileModuleService = container - .resolve(ModuleRegistrationName.FILE) - - const file = await fileModuleService.retrieve( - id - ) - - return new StepResponse(file.url) - } - ) - - type GetPurchasedProductMediaUrlWorkflowOutput = { - url: string - name: string - mime_type: string - } - - export const getPurchasedProductMediaUrlWorkflow = - createWorkflow< - GetPurchasedProductMediaUrlWorkflowInput, - GetPurchasedProductMediaUrlWorkflowOutput - >( - "get-purchased-product-media-url-workflow", - function (input) { - checkVariantPurchasedStep(input) - - const { product_media } = getProductMediaStep({ - variant_id: input.variant_id, - }) - - const url = getFileUrlStep({ - id: product_media.file_key, - }) - - const result = transform( - { - url, - product_media, - }, - (transformInput) => ({ - url: transformInput.url, - name: transformInput.product_media.name, - mime_type: transformInput.product_media.mime_type, - }) - ) - - return result - } - ) - ``` - - This workflow has three steps: - - 1. Check that the customer has purchased this product variant before. - 2. Retrieve the product media associated with the variant. - 3. Retrieve the pre-signed URL of the product media file. - 4. Return the details of the file. - - Then, create the file `src/api/store/digital-products/download/[variant_id]/route.ts` in the Medusa backend with the following content: - - ```ts title="src/api/store/digital-products/download/[variant_id]/route.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" - import type { - MedusaRequest, - MedusaResponse, - } from "@medusajs/medusa" - import { - getPurchasedProductMediaUrlWorkflow, - } from "../../../../../workflows/digital-product/get-url" - - export const GET = async ( - req: MedusaRequest, - res: MedusaResponse - ) => { - const { result } = await getPurchasedProductMediaUrlWorkflow( - req.scope - ).run({ - input: { - variant_id: req.params.variant_id, - customer_id: req.user.customer_id, - }, - }) - - res.json(result) - } - ``` - - Then, add the `authenticate` middleware to this API Route in `src/api/middlewares.ts`: - - ```ts title="src/api/middlewares.ts" - import { - type MiddlewaresConfig, - authenticate, - } from "@medusajs/medusa" - - export const config: MiddlewaresConfig = { - routes: [ - { - matcher: "/store/product-media/download/*", - middlewares: [ - authenticate("store", ["session", "bearer"]), - ], - }, - ], - } - ``` - - This ensures that only logged-in customers can access this API Route. - - {/* TODO check steps */} - - You can use this API Route in your storefront to add a button that allows downloading the purchased digital product. - - To mask the presigned URL, create a Next.js API route at `src/app/api/download/main/[variant_id]/route.ts` with the following content: - - ```ts title="src/app/api/download/main/[variant_id]/route.ts" badgeLabel="Storefront" badgeColor="blue" - import { NextRequest, NextResponse } from "next/server" - - export async function GET( - req: NextRequest, - { params }: { params: Record } - ) { - // Get the variant ID from the URL - const { variant_id } = params - - // Define the API URL - const apiUrl = `${ - process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL - }/store/product-media/download/${variant_id}` - - // Fetch the file data - const { - url, - name, - mime_type, - } = await fetch(apiUrl) - .then((res) => res.json()) - - // Handle the case where the file doesn't exist - // or the customer didn't purchase the product - if (!url) { - return new NextResponse( - "File doesn't exist", - { status: 401 } - ) - } - - // Fetch the file - const fileResponse = await fetch(url) - - // Handle the case where the file could not be fetched - if (!fileResponse.ok) { - return new NextResponse( - "File not found", - { status: 404 } - ) - } - - // Get the file content as a buffer - const fileBuffer = await fileResponse.arrayBuffer() - - // Define response headers - const headers = { - "Content-Type": mime_type, - // This sets the file name for the download - "Content-Disposition": `attachment; filename="${name}"`, - } - - // Create a NextResponse with the PDF content and headers - const response = new NextResponse(fileBuffer, { - status: 200, - headers, - }) - - return response - } - ``` - - Finally, add a button in the storefront that uses this route to allow customers to download the digital product after purchase. - - For example, you can change the `src/modules/order/components/item/index.tsx` file that handles showing each of the order's items in the order confirmation page to include a new download button if the customer is logged-in: - - ```tsx title="src/modules/order/components/item/index.tsx" badgeLabel="Storefront" badgeColor="blue" collapsibleLines="1-8" expandButtonLabel="Show Imports" - import { LineItem, Region } from "@medusajs/medusa" - import { Button, Table, Text, clx } from "@medusajs/ui" - import LineItemOptions from "@modules/common/components/line-item-options" - import LineItemPrice from "@modules/common/components/line-item-price" - import LineItemUnitPrice from "@modules/common/components/line-item-unit-price" - import Thumbnail from "@modules/products/components/thumbnail" - import { useAccount } from "../../../../lib/context/account-context" - - type ItemProps = { - item: Omit - region: Region - } - - const Item = ({ item, region }: ItemProps) => { - const { customer } = useAccount() - const handleDownload = async () => { - window.location.href = `${process.env.NEXT_PUBLIC_BASE_URL}/api/download/main/${item.variant_id}` - } - return ( - - -
- -
-
- - - {item.title} - - - - - - - {item.quantity}x - - - - - {customer && ( - - )} - - -
- ) - } - - export default Item - ``` - -
+Alternatively, you can build the storefront with your preferred tech stack. + +} + showLinkIcon={false} +/> diff --git a/www/apps/resources/app/recipes/integrate-ecommerce-stack/page.mdx b/www/apps/resources/app/recipes/integrate-ecommerce-stack/page.mdx index 0cc82561f3..f1e89eda42 100644 --- a/www/apps/resources/app/recipes/integrate-ecommerce-stack/page.mdx +++ b/www/apps/resources/app/recipes/integrate-ecommerce-stack/page.mdx @@ -102,7 +102,7 @@ export const serviceHighlights = [ - You can store the product's ID in the external system using the `metadata` field of the `Product` data model in the Product Module. Alternatively, you can create a [data model](!docs!/basics/data-models) in your module to store data related to the external system. + You can store the product's ID in the external system using the `metadata` property of the `Product` data model in the Product Module. Alternatively, you can create a [data model](!docs!/basics/data-models) in your module to store data related to the external system. @@ -203,13 +203,13 @@ export const workflowHighlights = [ .resolve(ModuleRegistrationName.PRODUCT) const createdProductData = await productModuleService - .retrieve(productId) + .retrieveProduct(productId) const erpProduct = await erpModuleService.createProduct( createdProductData ) - await productModuleService.update(productId, { + await productModuleService.updateProducts(productId, { metadata: { erp_id: erpProduct.id } @@ -229,7 +229,7 @@ export const workflowHighlights = [ .resolve(ModuleRegistrationName.PRODUCT) await erpModuleService.deleteProduct(erpId) - await productModuleService.update(productId, { + await productModuleService.updateProducts(productId, { metadata: {} }) }) @@ -249,7 +249,7 @@ export const workflowHighlights = [ - Retrieves the product's data using the Product Module's main service. - Create the product in the ERP system using the ERP Module's main service. - - Updates the product in Medusa by setting the ID of the ERP product in the product's `metadata` field. + - Updates the product in Medusa by setting the ID of the ERP product in the product's `metadata` property. The step also has a compensation function that rolls back changes when an error occurs. It deletes the product in the ERP system and removes the ID of the ERP product in the Medusa product. @@ -334,7 +334,7 @@ For example, suppose an administrator changes the product data in the ERP system ModuleRegistrationName.PRODUCT ) - await productService.update(id, updatedData) + await productService.updateProducts(id, updatedData) res.status(200) } diff --git a/www/apps/resources/app/recipes/marketplace/page.mdx b/www/apps/resources/app/recipes/marketplace/page.mdx index e463ab836e..4d83b02b3a 100644 --- a/www/apps/resources/app/recipes/marketplace/page.mdx +++ b/www/apps/resources/app/recipes/marketplace/page.mdx @@ -34,16 +34,22 @@ In a marketplace, an admin user has a store where they manage their products and Create a Marketplace Module that holds and manages these relationships. + + +Module Relationships is coming soon. + + + , showLinkIcon: false }, { - href: "/v2/advanced-development/modules/module-relationships", + href: "!docs!/advanced-development/modules/module-relationships", title: "Module Relationships", text: "Create relationships between modules.", startIcon: , @@ -60,80 +66,57 @@ Create a Marketplace Module that holds and manages these relationships. Then, create the file `src/modules/marketplace/models/store-user.ts` with the following content: ```ts title="src/modules/marketplace/models/store-user.ts" - import { BaseEntity } from "@medusajs/utils" - import { Entity, PrimaryKey, Property } from "@mikro-orm/core" - - @Entity() - class StoreUser extends BaseEntity { - @PrimaryKey() - id!: string - - @Property({ columnType: "text" }) - store_id: string - - @Property({ columnType: "text" }) - user_id: string - } - + import { model } from "@medusajs/utils" + + const StoreUser = model.define("store_user", { + id: model.id(), + store_id: model.text(), + user_id: model.text(), + }) + export default StoreUser ``` - This creates a `StoreUser` data model with the `store_id` and `user_id` fields. These fields will be used later to establish relationships to the Store and User modules. + This creates a `StoreUser` data model with the `store_id` and `user_id` properties. + + {/* These properties will be used later to establish relationships to the Store and User modules. */} Next, create the file `src/modules/marketplace/models/store-product.ts` with the following content: ```ts title="src/modules/marketplace/models/store-product.ts" - import { BaseEntity } from "@medusajs/utils" - import { Entity, PrimaryKey, Property } from "@mikro-orm/core" - - @Entity() - class StoreProduct extends BaseEntity { - @PrimaryKey() - id!: string - - @Property({ columnType: "text" }) - store_id: string - - @Property({ columnType: "text" }) - product_id: string - } - + import { model } from "@medusajs/utils" + + const StoreProduct = model.define("store_user", { + id: model.id(), + store_id: model.text(), + product_id: model.text(), + }) + export default StoreProduct ``` - This creates a `StoreProduct` data model with the `store_id` and `product_id` fields. These fields will be used later to establish relationships to the Store and Product modules. + This creates a `StoreProduct` data model with the `store_id` and `product_id` properties. + + {/* These properties will be used later to establish relationships to the Store and Product modules. */} Finally, create the file `src/modules/marketplace/models/store-order.ts` with the following content: ```ts title="src/modules/marketplace/models/store-order.ts" - import { BaseEntity } from "@medusajs/utils" - import { Entity, PrimaryKey, Property } from "@mikro-orm/core" - - @Entity() - class StoreOrder extends BaseEntity { - @PrimaryKey() - id!: string - - @Property({ columnType: "text" }) - store_id: string - - @Property({ columnType: "text" }) - order_id: string - - @Property({ columnType: "text" }) - parent_order_id?: string - } - + import { model } from "@medusajs/utils" + + const StoreOrder = model.define("store_user", { + id: model.id(), + store_id: model.text(), + order_id: model.text(), + parent_order_id: model.text(), + }) + export default StoreOrder ``` - This creates a `StoreOrder` data model with the `store_id`, `order_id`, and `parent_order_id` fields. The `store_id` and `order_id` fields will be used to establish relationships to the Store and Order modules. You’ll learn about the use of `parent_order_id` in a later section. + This creates a `StoreOrder` data model with the `store_id`, `order_id`, and `parent_order_id` properties. - - - You can add relationships to data models of other modules in a similar manner. - - + {/* The `store_id` and `order_id` properties will be used to establish relationships to the Store and Order modules. You’ll learn about the use of `parent_order_id` in a later section. */} To reflect these changes on the database, create the migration `src/modules/marketplace/migrations/Migration20240514143248.ts` with the following content: @@ -159,244 +142,34 @@ Create a Marketplace Module that holds and manages these relationships. } } - ``` You’ll run the migration after registering the module in the Medusa configurations. - - Next, you’ll create types that you’ll use throughout your customizations. - - Create the file `src/types/marketplace/index.ts` with the following content: - - ```ts title="src/types/marketplace/index.ts" - import { - StoreDTO, - UserDTO, - ProductDTO, - OrderDTO, - } from "@medusajs/types" - - export type StoreUserDTO = { - id: string - store_id: string - user_id: string - store?: StoreDTO - user?: UserDTO - } - - export type StoreProductDTO = { - id: string - store_id: string - product_id: string - store?: StoreDTO - product?: ProductDTO - } - - export type StoreOrderDTO = { - id: string - store_id: string - order_id: string - parent_order_id?: string - store?: StoreDTO - order?: OrderDTO - } - - export type CreateStoreUserDTO = { - store_id: string - user_id: string - } - - export type CreateStoreProductDTO = { - store_id: string - product_id: string - } - - export type CreateStoreOrderDTO = { - store_id: string - order_id: string - parent_order_id?: string - } - - ``` - + Then, create the module’s main service at `src/modules/marketplace/service.ts` with the following content: export const mainServiceHighlights = [ - ["48", "abstractModuleServiceFactory", "Extends the service factory to generate basic data management features."], - ["102", "relationships", "Defines relationships to the Store, User, Product, and Order modules."], - ["131", "create", "Method to create a `StoreUser`."], - ["141", "createStoreProduct", "Method to create a `StoreProduct`."], - ["150", "createStoreOrder", "Method to create a `StoreOrder`."] + ["6", "MedusaService", "Extends the service factory to generate data management features."] ] - ```ts title="src/modules/marketplace/service.ts" highlights={mainServiceHighlights} collapsibleLines="1-17" expandButtonLabel="Show Imports" - import { ModulesSdkUtils, Modules } from "@medusajs/utils" + ```ts title="src/modules/marketplace/service.ts" highlights={mainServiceHighlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" + import { MedusaService } from "@medusajs/utils" import StoreUser from "./models/store-user" import StoreProduct from "./models/store-product" import StoreOrder from "./models/store-order" - import { - CreateStoreOrderDTO, - CreateStoreProductDTO, - CreateStoreUserDTO, - StoreOrderDTO, - StoreProductDTO, - StoreUserDTO, - } from "../../types/marketplace" - import { - ModuleJoinerConfig, - ModulesSdkTypes, - } from "@medusajs/types" - - type InjectedDependencies = { - storeUserService: ModulesSdkTypes.InternalModuleService< - any - > - storeProductService: ModulesSdkTypes.InternalModuleService< - any - > - storeOrderService: ModulesSdkTypes.InternalModuleService< - any - > - } - - type AllModelsDTO = { - StoreUser: { - dto: StoreUserDTO - }, - StoreProduct: { - dto: StoreProductDTO - }, - StoreOrder: { - dto: StoreOrderDTO - } - } - - const generateMethodsFor = [ + + class MarketplaceModuleService extends MedusaService({ + StoreUser, StoreProduct, StoreOrder, - ] - - class MarketplaceModuleService extends ModulesSdkUtils - .abstractModuleServiceFactory< - InjectedDependencies, - StoreUserDTO, - AllModelsDTO - >( - StoreUser, generateMethodsFor - ) { - storeUserService_: ModulesSdkTypes.InternalModuleService< - StoreUser - > - storeProductService_: ModulesSdkTypes.InternalModuleService< - StoreProduct - > - storeOrderService_: ModulesSdkTypes.InternalModuleService< - StoreOrder - > - - constructor({ - storeUserService, - storeProductService, - storeOrderService, - }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - this.storeUserService_ = storeUserService - this.storeProductService_ = storeProductService - this.storeOrderService_ = storeOrderService - } - - __joinerConfig(): ModuleJoinerConfig { - return { - serviceName: "marketplaceModuleService", - alias: [ - { - name: ["store_user"], - args: { - entity: StoreUser.name, - }, - }, - { - name: ["store_product"], - args: { - entity: StoreProduct.name, - methodSuffix: "StoreProducts", - }, - }, - { - name: ["store_order"], - args: { - entity: StoreOrder.name, - methodSuffix: "StoreOrders", - }, - }, - ], - relationships: [ - { - serviceName: Modules.STORE, - alias: "store", - primaryKey: "id", - foreignKey: "store_id", - }, - { - serviceName: Modules.USER, - alias: "user", - primaryKey: "id", - foreignKey: "user_id", - }, - { - serviceName: Modules.PRODUCT, - alias: "product", - primaryKey: "id", - foreignKey: "product_id", - }, - { - serviceName: Modules.ORDER, - alias: "order", - primaryKey: "id", - foreignKey: "order_id", - }, - ], - } - } - - async create( - data: CreateStoreUserDTO - ): Promise { - const storeUser = await this.storeUserService_.create( - data - ) - - return storeUser - } - - async createStoreProduct( - data: CreateStoreProductDTO - ): Promise { - const storeProduct = await this.storeProductService_ - .create(data) - - return storeProduct - } - - async createStoreOrder( - data: CreateStoreOrderDTO - ): Promise { - const storeOrder = await this.storeOrderService_ - .create(data) - - return storeOrder - } - } - + }){ + // TODO add custom methods + } + export default MarketplaceModuleService ``` - The module’s main service: - - - Extends the service factory to generate basic data management features. - - Defines relationships to the Store, User, Product, and Order modules. - - Defines `create` methods for the `StoreUser`, `StoreProduct`, and `StoreOrder` data models. + The module’s main service extends the service factory to generate data management features for the `StoreUser`, `StoreProduct`, and `StoreOrder` data models. Finally, create the module definition at `src/modules/marketplace/index.ts` with the following content: @@ -439,39 +212,49 @@ export const mainServiceHighlights = [ To attach admin users to their own stores, create a subscriber that listens to the `user.created` event and attaches the user to the store. + + +- The `user.created` event is currently not emitted. +- Module Relationships is coming soon. + + + } showLinkIcon={false} /> -
+{/*
Create the file `src/subscribers/user-created.ts` with the following content: export const userSubscriberHighlights = [ - ["10", "", "The event data payload with the created user's ID."], - ["24", "", "Retrieve the created user."], - ["26", "", "Create a store for that user using the Store Module."], - ["30", "", "Create a relationship between the user and the store by creating a `StoreUser` record."] + ["13", "data", "The event data payload with the created user's ID."], + ["27", "retrieveUser", "Retrieve the created user."], + ["29", "createStores", "Create a store for that user using the Store Module."], + ["33", "createStoreUsers", "Create a relationship between the user and the store by creating a `StoreUser` record."] ] - ```ts title="src/subscribers/user-created.ts" highlights={userSubscriberHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" - import { SubscriberArgs, type SubscriberConfig } from "@medusajs/medusa" + ```ts title="src/subscribers/user-created.ts" highlights={userSubscriberHighlights} collapsibleLines="1-11" expandButtonLabel="Show Imports" + import type { + SubscriberArgs, + SubscriberConfig, + } from "@medusajs/medusa" import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { IUserModuleService, IStoreModuleService, } from "@medusajs/types" import MarketplaceModuleService from "../modules/marketplace/service" - + export default async function userCreatedHandler({ data, container, }: SubscriberArgs<{ id: string }>) { - const { id } = data.data || { data } + const { id } = "data" in data ? data.data : data const userModuleService: IUserModuleService = container.resolve( ModuleRegistrationName.USER ) @@ -481,21 +264,21 @@ export const userSubscriberHighlights = [ const marketplaceModuleService: MarketplaceModuleService = container.resolve( "marketplaceModuleService" ) - - const user = await userModuleService.retrieve(id) - - const store = await storeModuleService.create({ + + const user = await userModuleService.retrieveUser(id) + + const store = await storeModuleService.createStores({ name: `${user.first_name}'s Store`, }) - const storeUser = await marketplaceModuleService.create({ + const storeUser = await marketplaceModuleService.createStoreUsers({ store_id: store.id, user_id: user.id, }) - + console.log(`Created StoreUser ${storeUser.id}`) } - + export const config: SubscriberConfig = { event: "user.created", } @@ -515,7 +298,7 @@ export const userSubscriberHighlights = [ At the end of the output, you should see the message `Created StoreUser {store_user_id}` where the `{store_user_id}` is the ID of the created `StoreUser`. -
+
*/} --- @@ -523,37 +306,47 @@ export const userSubscriberHighlights = [ Similar to the previous section, to attach products to stores, create a subscriber that listens to the `product.created` event. In the subscriber, you attach the product to the store it’s created in. + + +- The `product.created` event is currently not emitted. +- Module Relationships is coming soon. + + + } showLinkIcon={false} /> -
+{/*
Create the file `src/subscribers/product-created.ts` with the following content: export const productSubscriberHighlights = [ - ["21", "", "Retrieve the created product."], - ["23", "", "This subscriber requires the store ID to be set in `product.metadata.store_id`. If not, the subscriber ends execution."], - ["27", "", "Create a relationship between the product and the store by creating a `StoreProduct` record."] + ["24", "retrieveProduct", "Retrieve the created product."], + ["26", "", "This subscriber requires the store ID to be set in `product.metadata.store_id`. If not, the subscriber ends execution."], + ["30", "createStoreProducts", "Create a relationship between the product and the store by creating a `StoreProduct` record."] ] - ```ts title="src/subscribers/product-created.ts" highlights={productSubscriberHighlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" - import { SubscriberArgs, type SubscriberConfig } from "@medusajs/medusa" + ```ts title="src/subscribers/product-created.ts" highlights={productSubscriberHighlights} collapsibleLines="1-10" expandButtonLabel="Show Imports" + import type { + SubscriberArgs, + SubscriberConfig, + } from "@medusajs/medusa" import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { IProductModuleService, } from "@medusajs/types" import MarketplaceModuleService from "../modules/marketplace/service" - + export default async function productCreateHandler({ data, container, }: SubscriberArgs<{ id: string }>) { - const { id } = data.data || data + const { id } = "data" in data ? data.data : data const productModuleService: IProductModuleService = container.resolve( ModuleRegistrationName.PRODUCT ) @@ -561,19 +354,19 @@ export const productSubscriberHighlights = [ const marketplaceModuleService: MarketplaceModuleService = container.resolve( "marketplaceModuleService" ) - - const product = await productModuleService.retrieve(id) - + + const product = await productModuleService.retrieveProduct(id) + if (!product.metadata?.store_id) { return } - - await marketplaceModuleService.createStoreProduct({ + + await marketplaceModuleService.createStoreProducts({ store_id: product.metadata.store_id as string, product_id: id, }) } - + export const config: SubscriberConfig = { event: "product.created", } @@ -607,7 +400,7 @@ export const productSubscriberHighlights = [ This returns the created product. In the next section, you’ll implement the API route to fetch the products of a store. -
+
*/} --- @@ -616,16 +409,22 @@ export const productSubscriberHighlights = [ To allow admin users to view their store’s products, create an API route that uses the remote query to fetch the products based on the logged-in user’s store. + + +Retrieving module relationship data using the remote query is coming soon. + + + , showLinkIcon: false }, { - href: "/v2/advanced-development/modules/remote-query", + href: "!docs!/advanced-development/modules/remote-query", title: "Remote Query", text: "Use the remote query to fetch data across modules.", startIcon: , @@ -633,14 +432,14 @@ To allow admin users to view their store’s products, create an API route that }, ]} /> -
+{/*
Create the file `src/api/admin/marketplace/products/route.ts` with the following content: export const productRoutesHighlights = [ - ["26", "", "Retrieve the store of the logged-in user."], - ["37", "", "Build a query that retrieves the products of that store."], - ["49", "", "Retrieve the products using remote query."] + ["26", "storeUsers", "Retrieve the store of the logged-in user."], + ["38", "query", "Build a query that retrieves the products of that store."], + ["50", "remoteQuery", "Retrieve the products using remote query."] ] ```ts title="src/api/admin/marketplace/products/route.ts" highlights={productRoutesHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports" @@ -656,7 +455,7 @@ export const productRoutesHighlights = [ } from "@medusajs/utils" import MarketplaceModuleService from "../../../../modules/marketplace/service" - + export async function GET( req: AuthenticatedMedusaRequest, res: MedusaResponse @@ -668,18 +467,19 @@ export const productRoutesHighlights = [ const remoteQuery: RemoteQueryFunction = req.scope.resolve( ContainerRegistrationKeys.REMOTE_QUERY ) - - const storeUsers = await marketplaceModuleService.list({ - user_id: req.auth.actor_id, - }) - + + const storeUsers = await marketplaceModuleService + .listStoreUsers({ + user_id: req.auth_context.actor_id, + }) + if (!storeUsers.length) { throw new MedusaError( MedusaError.Types.NOT_FOUND, "This user doesn't have an associated store." ) } - + const query = remoteQueryObjectFromString({ entryPoint: "store_product", fields: [ @@ -691,9 +491,9 @@ export const productRoutesHighlights = [ }, }, }) - + const result = await remoteQuery(query) - + res.json({ store_id: storeUsers[0].store_id, products: result.map((data) => data.product), @@ -741,39 +541,39 @@ export const productRoutesHighlights = [ This will return the product you created in the previous section, if the `{jwt_token}` belongs to the user of the same store ID. -
+
*/} --- ## Split Orders Based on Stores +An order may contain items from different stores. To ensure that users can only view and manage their orders, create a subscriber that listens to the `order.placed` event and handles splitting the order into multiple orders based on the items’ stores. + -While this section showcases the implementation, the `order.placed` event is still not emitted in Medusa V2. +The `order.placed` event is still not emitted in Medusa V2. -An order may contain items from different stores. To ensure that users can only view and manage their orders, create a subscriber that listens to the `order.placed` event and handles splitting the order into multiple orders based on the items’ stores. - } showLinkIcon={false} /> -
+{/*
Create the file `src/subscribers/order-created.ts` with the following content: export const orderSubscriberHighlights = [ ["35", "", "Loop over the created order’s items."], ["57", "", "Group the items by their store ID."], - ["72", "", "If the items have the same store ID, then associate the created order with the store."], - ["88", "", "If there are items from more than one store in the order, create child orders and associate each of them with the store."], - ["91", "parent_order_id", "The `parent_order_id` field in the `StoreOrder` data model points to the original order."] + ["72", "createStoreOrders", "If the items have the same store ID, then associate the created order with the store."], + ["88", "createStoreOrders", "If there are items from more than one store in the order, create child orders and associate each of them with the store."], + ["91", "parent_order_id", "The `parent_order_id` property in the `StoreOrder` data model points to the original order."] ] ```ts title="src/subscribers/order-created.ts" highlights={orderSubscriberHighlights} collapsibleLines="1-13" expandButtonLabel="Show Imports" @@ -789,12 +589,12 @@ export const orderSubscriberHighlights = [ import MarketplaceModuleService from "../modules/marketplace/service" import { createOrdersWorkflow } from "@medusajs/core-flows" - + export default async function orderCreatedHandler({ data, container, }: SubscriberArgs<{ id: string }>) { - const { id } = data.data || data + const { id } = "data" in data ? data.data : data const orderModuleService: IOrderModuleService = container.resolve( ModuleRegistrationName.ORDER @@ -804,23 +604,23 @@ export const orderSubscriberHighlights = [ container.resolve( "marketplaceModuleService" ) - + const storeToOrders: Record = {} - - const order = await orderModuleService.retrieve(id, { + + const order = await orderModuleService.retrieveOrder(id, { relations: ["items"], }) - + await Promise.all(order.items?.map(async (item) => { const storeProduct = await marketplaceModuleService .listStoreProducts({ product_id: item.product_id, }) - + if (!storeProduct.length) { return } - + const storeId = storeProduct[0].store_id if (!storeToOrders[storeId]) { @@ -830,32 +630,32 @@ export const orderSubscriberHighlights = [ items: [], } } - + const { id, ...itemDetails } = item - + storeToOrders[storeId].items.push(itemDetails) })) - + const storeToOrdersKeys = Object.keys(storeToOrders) - + if (!storeToOrdersKeys.length) { return } - + if ( storeToOrdersKeys.length === 1 && storeToOrders[0].items.length === order.items.length ) { // The order is composed of items from one store, so // associate the order as-is with the store. - await marketplaceModuleService.createStoreOrder({ + await marketplaceModuleService.createStoreOrders({ store_id: storeToOrdersKeys[0], order_id: order.id, }) - + return } - + // create store orders for each child order await Promise.all( storeToOrdersKeys.map(async (storeId) => { @@ -863,8 +663,8 @@ export const orderSubscriberHighlights = [ .run({ input: storeToOrders[storeId], }) - - await marketplaceModuleService.createStoreOrder({ + + await marketplaceModuleService.createStoreOrders({ store_id: storeId, order_id: result.id, parent_order_id: order.id, @@ -872,7 +672,7 @@ export const orderSubscriberHighlights = [ }) ) } - + export const config: SubscriberConfig = { event: "order.placed", } @@ -883,13 +683,13 @@ export const orderSubscriberHighlights = [ - Loop over the created order’s items. - Group the items by their store ID. - If the items have the same store ID, then associate the created order with the store. - - If there are items from more than one store in the order, create child orders and associate each of them with the store. Here, you use the `parent_order_id` field in the `StoreOrder` data model to point to the original order. + - If there are items from more than one store in the order, create child orders and associate each of them with the store. Here, you use the `parent_order_id` property in the `StoreOrder` data model to point to the original order. To test this out, create an order in your store. That will run the subscriber and create the child orders. The next section covers how to retrieve the store’s orders. -
+
*/} --- @@ -898,16 +698,22 @@ export const orderSubscriberHighlights = [ Similar to products, to allow admin users to view their store’s orders, create an API route that uses the remote query to fetch the orders based on the logged-in user’s store. + + +Retrieving module relationship data using the remote query is coming soon. + + + , showLinkIcon: false }, { - href: "/v2/advanced-development/modules/remote-query", + href: "!docs!/advanced-development/modules/remote-query", title: "Remote Query", text: "Use the remote query to fetch data across modules.", startIcon: , @@ -915,7 +721,7 @@ Similar to products, to allow admin users to view their store’s orders, create }, ]} /> -
+{/*
Create the file `src/api/admin/marketplace/orders/route.ts` with the following content: @@ -937,7 +743,7 @@ export const orderRoutesHighlights = [ } from "@medusajs/utils" import MarketplaceModuleService from "../../../../modules/marketplace/service" - + export async function GET( req: AuthenticatedMedusaRequest, res: MedusaResponse @@ -949,11 +755,11 @@ export const orderRoutesHighlights = [ const remoteQuery: RemoteQueryFunction = req.scope.resolve( ContainerRegistrationKeys.REMOTE_QUERY ) - - const storeUsers = await marketplaceModuleService.list({ - user_id: req.auth.actor_id, + + const storeUsers = await marketplaceModuleService.listStoreUsers({ + user_id: req.auth_context.actor_id, }) - + const query = remoteQueryObjectFromString({ entryPoint: "store_order", fields: [ @@ -965,14 +771,13 @@ export const orderRoutesHighlights = [ }, }, }) - + const result = await remoteQuery(query) - + res.json({ orders: result.map((data) => data.order), }) } - ``` This creates a `GET` API route at `/admin/marketplace/orders`. In the API route, you: @@ -990,7 +795,7 @@ export const orderRoutesHighlights = [ This will return the orders you created in the previous section if the `{jwt_token}` belongs to the user of the same store ID. -
+
*/} --- diff --git a/www/apps/resources/app/recipes/multi-region-store/page.mdx b/www/apps/resources/app/recipes/multi-region-store/page.mdx index 37ca3edf9c..847ed9697b 100644 --- a/www/apps/resources/app/recipes/multi-region-store/page.mdx +++ b/www/apps/resources/app/recipes/multi-region-store/page.mdx @@ -114,15 +114,11 @@ Using the tax-inclusive feature, merchants can specify prices including taxes pe } }, { - href: "#", + href: "/storefront-development/products/price", title: "Display Product Price in Storefront", text: "Learn how to display the correct product price in a storefront.", startIcon: , showLinkIcon: false, - badge: { - variant: "blue", - children: "Guide Soon" - } }, ]} /> diff --git a/www/apps/resources/app/recipes/personalized-products/page.mdx b/www/apps/resources/app/recipes/personalized-products/page.mdx index c7a8f631c2..c96311c4e9 100644 --- a/www/apps/resources/app/recipes/personalized-products/page.mdx +++ b/www/apps/resources/app/recipes/personalized-products/page.mdx @@ -21,7 +21,7 @@ You also have freedom in how you choose to implement the storefront, allowing yo ## Store Personalized Data -The Cart Module's `LineItem` data model has a `metadata` field that holds any custom data. You can pass the customer's customization in that field when adding a product to the cart. +The Cart Module's `LineItem` data model has a `metadata` property that holds any custom data. You can pass the customer's customization in the request body's `metadata` field when adding a product to the cart. For example, if you’re asking customers to enter a message to put in a letter they’re purchasing, use the `metadata` attribute of the `LineItem` data model to set the personalized information entered by the customer: @@ -81,9 +81,9 @@ Medusa provides a Next.js Starter storefront with basic ecommerce functionalitie showLinkIcon: false }, { - href: "!docs!/storefront-development/tips", - title: "Build Your Own Storefront", - text: "Find tips on how to create a storefront.", + href: "/storefront-development", + title: "Storefront Development", + text: "Find guides for your storefront development.", startIcon: , showLinkIcon: false }, diff --git a/www/apps/resources/app/recipes/pos/page.mdx b/www/apps/resources/app/recipes/pos/page.mdx index 6a5c977e6c..639ed99e2f 100644 --- a/www/apps/resources/app/recipes/pos/page.mdx +++ b/www/apps/resources/app/recipes/pos/page.mdx @@ -45,7 +45,7 @@ Medusa's modular architecture removes any restrictions you may have while making POS systems make the checkout process smoother by integrating a barcode scanner. Merchants scan a product by its barcode to check its details or add it to the customer's purchase. -The Product Module's `ProductVariant` data model has the fields to implement this integration, mainly the `barcode` attribute. Other notable fields include `ean`, `upc`, and `hs_code`, among others. +The Product Module's `ProductVariant` data model has the properties to implement this integration, mainly the `barcode` attribute. Other notable properties include `ean`, `upc`, and `hs_code`, among others. To search through product variants by their barcode, create a custom API Route and call it within your POS. @@ -53,14 +53,14 @@ To search through product variants by their barcode, create a custom API Route a , showLinkIcon: false }, { - href: "https://docs.medusajs.com/api/admin", + href: "!docs!/basics/api-routes", title: "Create API Route", text: "Learn how to create an API Route.", startIcon: , @@ -100,7 +100,7 @@ To search through product variants by their barcode, create a custom API Route a // retrieve product variants by barcode const productVariants = await productModuleService - .listVariants({ + .listProductVariants({ // @ts-ignore barcode, }) diff --git a/www/apps/resources/app/recipes/subscriptions/page.mdx b/www/apps/resources/app/recipes/subscriptions/page.mdx index 6c8243c595..7449a9ecc2 100644 --- a/www/apps/resources/app/recipes/subscriptions/page.mdx +++ b/www/apps/resources/app/recipes/subscriptions/page.mdx @@ -141,9 +141,9 @@ Medusa provides a Next.js Starter. Since you've customized your Medusa project, showLinkIcon: false }, { - href: "!docs!/storefront-development/tips", + href: "/storefront-development", title: "Option 2: Build Custom Storefront", - text: "Find tips to build your own storefront.", + text: "Find guides for your storefront development.", startIcon: , showLinkIcon: false } diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index 1466b22e6a..c225934e63 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -1,104 +1,4 @@ export const filesMap = [ - { - "filePath": "/www/apps/resources/app/_plugins/analytics/segment/page.mdx", - "pathname": "/_plugins/analytics/segment" - }, - { - "filePath": "/www/apps/resources/app/_plugins/cms/contentful/page.mdx", - "pathname": "/_plugins/cms/contentful" - }, - { - "filePath": "/www/apps/resources/app/_plugins/cms/strapi/page.mdx", - "pathname": "/_plugins/cms/strapi" - }, - { - "filePath": "/www/apps/resources/app/_plugins/erp/brightpearl/page.mdx", - "pathname": "/_plugins/erp/brightpearl" - }, - { - "filePath": "/www/apps/resources/app/_plugins/fulfillment/manual/page.mdx", - "pathname": "/_plugins/fulfillment/manual" - }, - { - "filePath": "/www/apps/resources/app/_plugins/fulfillment/webshipper/page.mdx", - "pathname": "/_plugins/fulfillment/webshipper" - }, - { - "filePath": "/www/apps/resources/app/_plugins/notifications/mailchimp/page.mdx", - "pathname": "/_plugins/notifications/mailchimp" - }, - { - "filePath": "/www/apps/resources/app/_plugins/notifications/sendgrid/page.mdx", - "pathname": "/_plugins/notifications/sendgrid" - }, - { - "filePath": "/www/apps/resources/app/_plugins/notifications/slack/page.mdx", - "pathname": "/_plugins/notifications/slack" - }, - { - "filePath": "/www/apps/resources/app/_plugins/notifications/twilio-sms/page.mdx", - "pathname": "/_plugins/notifications/twilio-sms" - }, - { - "filePath": "/www/apps/resources/app/_plugins/other/discount-generator/page.mdx", - "pathname": "/_plugins/other/discount-generator" - }, - { - "filePath": "/www/apps/resources/app/_plugins/other/ip-lookup/page.mdx", - "pathname": "/_plugins/other/ip-lookup" - }, - { - "filePath": "/www/apps/resources/app/_plugins/other/restock-notifications/page.mdx", - "pathname": "/_plugins/other/restock-notifications" - }, - { - "filePath": "/www/apps/resources/app/_plugins/other/wishlist/page.mdx", - "pathname": "/_plugins/other/wishlist" - }, - { - "filePath": "/www/apps/resources/app/_plugins/page.mdx", - "pathname": "/_plugins" - }, - { - "filePath": "/www/apps/resources/app/_plugins/payment/klarna/page.mdx", - "pathname": "/_plugins/payment/klarna" - }, - { - "filePath": "/www/apps/resources/app/_plugins/payment/paypal/page.mdx", - "pathname": "/_plugins/payment/paypal" - }, - { - "filePath": "/www/apps/resources/app/_plugins/payment/stripe/page.mdx", - "pathname": "/_plugins/payment/stripe" - }, - { - "filePath": "/www/apps/resources/app/_plugins/search/algolia/page.mdx", - "pathname": "/_plugins/search/algolia" - }, - { - "filePath": "/www/apps/resources/app/_plugins/search/meilisearch/page.mdx", - "pathname": "/_plugins/search/meilisearch" - }, - { - "filePath": "/www/apps/resources/app/_plugins/source/shopify/page.mdx", - "pathname": "/_plugins/source/shopify" - }, - { - "filePath": "/www/apps/resources/app/_plugins/storage/local/page.mdx", - "pathname": "/_plugins/storage/local" - }, - { - "filePath": "/www/apps/resources/app/_plugins/storage/minio/page.mdx", - "pathname": "/_plugins/storage/minio" - }, - { - "filePath": "/www/apps/resources/app/_plugins/storage/s3/page.mdx", - "pathname": "/_plugins/storage/s3" - }, - { - "filePath": "/www/apps/resources/app/_plugins/storage/spaces/page.mdx", - "pathname": "/_plugins/storage/spaces" - }, { "filePath": "/www/apps/resources/app/admin-widget-injection-zones/page.mdx", "pathname": "/admin-widget-injection-zones" diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 934b7ad537..a949eae761 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -1721,7 +1721,7 @@ export const sidebar = sidebarAttachHrefCommonOptions([ children: [ { path: "/references/file-provider-module", - title: "Create File Module", + title: "Create File Provider", }, ], }, @@ -1749,7 +1749,7 @@ export const sidebar = sidebarAttachHrefCommonOptions([ }, { path: "/references/notification-provider-module", - title: "Create Notification Module", + title: "Create Notification Provider", }, ], }, diff --git a/www/packages/docs-ui/src/components/CodeBlock/Line/index.tsx b/www/packages/docs-ui/src/components/CodeBlock/Line/index.tsx index 1f276e31d6..01c38f04f4 100644 --- a/www/packages/docs-ui/src/components/CodeBlock/Line/index.tsx +++ b/www/packages/docs-ui/src/components/CodeBlock/Line/index.tsx @@ -23,7 +23,6 @@ type CodeBlockLineProps = { showLineNumber: boolean lineNumberColorClassName: string lineNumberBgClassName: string - noLineNumbers?: boolean } & Pick export const CodeBlockLine = ({ diff --git a/www/packages/docs-ui/src/components/CodeBlock/index.tsx b/www/packages/docs-ui/src/components/CodeBlock/index.tsx index 6061d953ae..74ac6c903d 100644 --- a/www/packages/docs-ui/src/components/CodeBlock/index.tsx +++ b/www/packages/docs-ui/src/components/CodeBlock/index.tsx @@ -334,7 +334,8 @@ export const CodeBlock = ({ !hasInnerCodeBlock && tokens.length <= 1 && "px-docs_0.5 py-[6px]", - !title.length && "xs:max-w-[83%]", + !title.length && (!noCopy || !noReport) && "xs:max-w-[83%]", + noLineNumbers && "pl-docs_1", preClassName )} >