diff --git a/www/docs/content/plugins/other/restock-notifications.md b/www/docs/content/plugins/other/restock-notifications.md new file mode 100644 index 0000000000..51bec1e6f2 --- /dev/null +++ b/www/docs/content/plugins/other/restock-notifications.md @@ -0,0 +1,177 @@ +--- +addHowToData: true +--- + +# Restock Notifications Plugin + +In this document, you’ll learn how to install the restock notification plugin on your Medusa backend. + +:::note + +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, you need to use a [notification plugin](../notifications/). + +::: + +## Overview + +Customers browsing your products may find something that they need, but it's unfortunately out of stock. In this scenario, you can keep them interested in your product and, subsequently, in your store by notifying them when the product is back in stock. + +The Restock Notifications plugin provides new endpoints that allow the customer to subscribe 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. The event's payload includes the ID of the product variant and the customer emails subscribed to it. You can pair this with a subscriber that listens to that event and sends a notification to the customer using a [Notification plugin](../notifications/). + +--- + +## Prerequisites + +### Medusa Backend + +Before you follow this guide, you must have a Medusa backend installed. If not, you can follow the [quickstart guide](../../create-medusa-app.mdx) to learn how to do it. + +### Event-Bus Module + +To trigger events to the subscribed handler methods, you must have an event-bus module installed. For development purposes, you can use the [Local module](../../development/events/modules/local.md) which should be enabled by default in your Medusa backend. + +For production, it's recommended to use the [Redis module](../../development/events/modules/redis.md). + +--- + +## Install Plugin + +In the root directory of your Medusa backend, run the following command to install the Restock Notifications plugin: + +```bash npm2yarn +npm install medusa-plugin-restock-notification +``` + +Then, add the plugin into the plugins array exported as part of the Medusa configuration in `medusa-config.js`: + +```js title=medusa-config.js +const plugins = [ + // other plugins... + { + resolve: `medusa-plugin-restock-notification`, + options: { + // optional options + trigger_delay, // delay time in milliseconds + inventory_required, // minimum restock inventory quantity + }, + }, +] +``` + +The plugin accepts the following optional options: + +1. `trigger_delay`: a number indicating the time in milliseconds to delay the triggering of the `restock-notification.restocked` event. Default value is `0`. +2. `inventory_required`: a number indicating the minimum inventory quantity to consider a product variant as restocked. Default value is `0`. + +Finally, run the migrations of this plugin before you start the Medusa backend: + +```bash +npx medusa migrations run +``` + +--- + +## Test Plugin + +### 1. Run Medusa Backend + +In the root of your Medusa backend project, run the following command to start the Medusa backend: + +```bash npm2yarn +npm run start +``` + +### 2. Subscribe to Variant Restock Notifications + +Then, send a `POST` request to the endpoint `/restock-notifications/variants/` to subscribe to restock notifications of a product variant ID. Note that `` refers to the URL fo your Medusa backend, which is `http://localhost:9000` during development, and `` refers to the ID of the product variant you're subscribing to. + +:::note + +You can only subscribe to product variants that are out-of-stock. Otherwise, you'll receive an error. + +::: + +The endpoint 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. This is useful if you're using multi-warehouse modules, as the product variant's quantity is checked correctly when checking if it's out of stock. Alternatively, you can pass the [publishable API key in the header of the request](../../development/publishable-api-keys/storefront/use-in-requests.md) and the sales channel will be derived from it. + +### 3. Trigger Restock Notification + +After subscribing to the out-of-stock variant, change its stock quantity to the minimum inventory required to test out the event trigger. The new stock quantity should be any value above `0` if you didn't set the `inventory_required` option. + +You can use the [Medusa admin](../../user-guide/products/manage.mdx#manage-product-variants) or the [Admin REST API endpoints](https://docs.medusajs.com/api/admin#products_postproductsproductvariantsvariant) to update the quantity. + +After you update the quantity, you can see the `restock-notification.restocked` triggered and logged in the Medusa backend logs. If you've implemented the notification sending, this is where it'll be triggered and a notification will be sent. + +--- + +## Example: Implement Notification Sending with SendGrid + +:::note + +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 you can send a notification to the customer using a Notication plugin. + +::: + +Here's an example of a subscriber that listens to the `restock-notification.restocked` event and uses the [SendGrid plugin](../notifications/sendgrid.mdx) to send the subscribed customers an email: + +```ts title=src/subscribers/restock-notification.ts +import { + EventBusService, + ProductVariantService, +} from "@medusajs/medusa" + +type InjectedDependencies = { + eventBusService: EventBusService, + sendgridService: any + productVariantService: ProductVariantService +} + +class RestockNotificationSubscriber { + protected sendGridService_: any + protected productVariantService_: ProductVariantService + + constructor({ + eventBusService, + sendgridService, + productVariantService, + }: InjectedDependencies) { + this.sendGridService_ = sendgridService + this.productVariantService_ = productVariantService + eventBusService.subscribe( + "restock-notification.restocked", + this.handleRestockNotification + ) + } + + handleRestockNotification = async ({ + variant_id, + emails, + }) => { + // retrieve variant + const variant = await this.productVariantService_.retrieve( + variant_id + ) + + this.sendGridService_.sendEmail({ + templateId: "restock-notification", + from: "hello@medusajs.com", + to: emails, + data: { + // any data necessary for your template... + variant, + }, + }) + } +} + +export default RestockNotificationSubscriber +``` + +Handler methods subscribed to the `restock-notification.restocked` event, which in this case is the `handleRestockNotification` method, receive the following object data payload as a parameter: + +- `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. Here, you pass it along to the SendGrid plugin directly to send the email to everyone subscribed. If necessary, you can also retrieve the customer of that email using the `CustomerService`'s [retrieveByEmail](../../references/services/classes/CustomerService.md#retrievebyemail) method. + +In the method, you retrieve the variant by its ID using the `ProductVariantService`, then send the email using the SendGrid plugins' `SendGridService`. diff --git a/www/docs/content/recipes/commerce-automation.mdx b/www/docs/content/recipes/commerce-automation.mdx new file mode 100644 index 0000000000..a54cf5ef91 --- /dev/null +++ b/www/docs/content/recipes/commerce-automation.mdx @@ -0,0 +1,689 @@ +--- +addHowToData: true +--- + +import DocCardList from '@theme/DocCardList'; +import DocCard from '@theme/DocCard'; +import Icons from '@theme/Icon'; + +# Commerce automation + +This document guides you on how you can implement different forms of commerce automation within your Medusa project. + +## Overview + +Commerce automation is essential for businesses that want to save costs, provide better user experience, and avoid manual, repetitive tasks that can lead to human errors. Businesses can utilize it in different domains, including marketing, customer support, order management, and more. + +Medusa provides you with the necessary architecture and resources to implement commerce automation related to order management, customer service, and more. Mainly, it utilizes an event bus that allows you to listen to certain event triggers and perform asynchronously actions. + +This document gives a high-level overview of how you can implement different types of commerce automation. + +--- + +## Re-Stock Notifications + +Customers may be interested in a product that is currently out of stock. + +Medusa already provides a [Re-stock notifications plugin](../plugins/other/restock-notifications.md). You can also implement this by creating custom entities, triggering custom events, and handling these events with a subscriber. + + + +--- + +## Automated Customer Support + +Customer support is essential to build a store’s brand and customer loyalty. This can include integrating with third-party services or automating notifications sent to customers when changes happen related to their orders, returns, exchanges, and more. + +Using Medusa’s Notification Service, you can handle notifications triggered from actions on the customer’s orders or profile. + +For example, when an order's status is updated, the `order.updated` event is triggered. You can create a Notification Service that handles this event by sending the customer an email informing them of the status update. + +The [Event reference](../development/events/events-list.md) includes an extensive list of events triggered by the Medusa core. + +Medusa also provides official notification plugins that integrate with third-party services, such as [SendGrid](../plugins/notifications/sendgrid.mdx) or [TwilioSMS](../plugins/notifications/twilio-sms.md). + + + +
+ +Example: Sending an email for new notes + + +Here’s an example of a subscriber that uses the SendGrid plugin to send an email to the customer when the order has a new note: + +```ts title=src/subscribers/new-note.ts +import { + EventBusService, + NoteService, + OrderService, +} from "@medusajs/medusa" + +type InjectedDependencies = { + eventBusService: EventBusService + sendgridService: any + noteService: NoteService + orderService: OrderService +} + +class NewNoteSubscriber { + protected sendGridService_: any + protected noteService_: NoteService + protected orderService_: OrderService + + constructor({ + eventBusService, + sendgridService, + noteService, + orderService, + }: InjectedDependencies) { + this.noteService_ = noteService + this.orderService_ = orderService + this.sendGridService_ = sendgridService + eventBusService.subscribe( + "note.created", + this.handleNoteCreated + ) + } + + handleNoteCreated = async (data) => { + // retrieve note by id + const note = await this.noteService_.retrieve(data.id, { + relations: ["author"], + }) + + if (!note || note.resource_type !== "order") { + return + } + + // retrieve note's order + const order = await this.orderService_.retrieve( + note.resource_id + ) + + if (!order) { + return + } + + this.sendGridService_.sendEmail({ + templateId: "order-update", + from: "hello@medusajs.com", + to: order.email, + data: { + // any data necessary for your template... + note_text: note.value, + note_author: note.author.first_name, + note_date: note.created_at, + order_id: order.display_id, + }, + }) + } +} + +export default NewNoteSubscriber +``` + +
+ +--- + +## Automatic Data Synchronization + +As your commerce store gets bigger, you’ll likely need to synchronize data across different systems. For example, synchronizing data with an ERP system or a data warehouse. + +You can implement automatic synchronization in Medusa using scheduled jobs. A scheduled job runs at a specified date and time interval in the background of your Medusa backend. Within the scheduled job, you can synchronize your internal or external data. + + + +
+ +Example: Synchronizing products with a third-party service + + +Here’s an example of synchronizing products with a third party service using a [loader](../development/loaders/create.md): + +```ts title=src/loaders/sync-products.ts +import { + Logger, + ProductService, + StoreService, +} from "@medusajs/medusa" +import { + ProductSelector, +} from "@medusajs/medusa/dist/types/product" +import { AwilixContainer } from "awilix" + +export default async ( + container: AwilixContainer, + config: Record +): Promise => { + const logger = container.resolve("logger") + logger.info("Synchronizing products...") + const productService = container.resolve( + "productService" + ) + const storeService = container.resolve( + "storeService" + ) + // retrieve store to get last sync date + const store = await storeService.retrieve() + + const productFilters: ProductSelector = {} + + if (store.metadata.last_sync_date) { + productFilters.updated_at = { + gt: new Date( + store.metadata.last_sync_date as string + ), + } + } + + const updatedProducts = await productService.list( + productFilters + ) + + updatedProducts.forEach((product) => { + // assuming client is an initialized connection + // with a third-party service + client.sync(product) + }) + + await storeService.update({ + metadata: { + last_sync_date: new Date(), + }, + }) + + logger.info("Finish synchronizing products") +} +``` + +Notice that here it’s assumed that: + +1. The last update date is stored in the `Store`'s metadata object. You can instead use a custom entity to handle this. +2. The connection to the third-party service is assumed to be available and handled within the `client` variable. + +
+ +--- + +## Order Management Automation + +Using Medusa’s architecture and commerce features, you can automate a large amount of order management functionalities. + +By utilizing the event bus service, you can handle events within an order workflow to automate actions. For example, when an order is placed, you can listen to the `order.placed` event and automatically create a fulfillment if a set of predefined conditions are met. + +Another example is automatically capturing payment when an order is fully shipped and the `order.shipment_created` is triggered. + + + +--- + +## Automated RMA Flow + +Business need to optimize their Return Merchandise Authorization (RMA) flow to ensure a good customer experience and service. By automating the flow, customers can request to return their received items, and businesses can quickly support them. + +Medusa’s commerce features are geared towards automating RMA flows and ensuring a good customer experience. + +For example, customers can create returns for their orders without direct involvement from the store operator. The store operator will then receive a notification regarding the return and handle it accordingly. The same applies to order exchanges. + +Medusa also allows the store operator to edit orders, receive customer approval for the edits, and authorize additional payment if necessary, all within a seamless workflow. + +Similar to the examples mentioned earlier, each of these actions trigger events that you can listen to and perform additional actions based on your use case. + + + +--- + +## Customer Segmentation + +Businesses use Customer Segmentation to oragnize customers into different groups, then apply different price rules to these groups. For example, you may group customers by their product preferences, the number of orders they’ve placed, or geographical location. + +Based on your use case and the logic of segmentation, you can use different Medusa automation development tools to detect a customer’s segment. Medusa also provides a Customer Groups feature that allows you to segment customers, whether you do it manually or automatically. + +For example, if you’re grouping together customers with more than twenty orders, you can use a Subscriber that listens to the `order.placed` event and checks the number of orders the customer has so far. Then, using Medusa’s Customer Groups feature, you can add that customer to the VIP group. Finally, you can utilize the Price Lists feature to provide different prices or discounts for customers in the VIP group. + + + +
+ +Example: Add customer to VIP group + + +Here’s an example of a subscriber that listens to the `order.placed` event and checks if the customer should be added to the VIP customer group based on their number of orders: + +```ts title=src/subscribers/add-custom-to-vip.ts +import { + CustomerGroupService, + CustomerService, + EventBusService, + OrderService, +} from "@medusajs/medusa" + +type InjectedDependencies = { + orderService: OrderService + customerService: CustomerService + customerGroupService: CustomerGroupService + eventBusService: EventBusService +} + +class AddCustomerToVipSubscriber { + protected orderService_: OrderService + protected customerService_: CustomerService + protected customerGroupService_: CustomerGroupService + + constructor({ + orderService, + customerService, + customerGroupService, + eventBusService, + }: InjectedDependencies) { + this.orderService_ = orderService + this.customerService_ = customerService + this.customerGroupService_ = customerGroupService + eventBusService.subscribe( + "order.placed", + this.handleOrderPlaced + ) + } + + handleOrderPlaced = async ({ id }) => { + // check if VIP group exists + const vipGroup = await this.customerGroupService_.list({ + name: "VIP", + }, { + relations: ["customers"], + }) + if (!vipGroup.length) { + return + } + + // retrieve order and its customer + const order = await this.orderService_.retrieve(id) + + if (!order || !order.customer_id || + vipGroup[0].customers.find( + (customer) => customer.id === order.customer_id + ) !== undefined) { + return + } + + // retrieve orders of this customer + const [, count] = await this.orderService_.listAndCount({ + customer_id: order.customer_id, + }) + + if (count >= 20) { + // add customer to VIP group + this.customerGroupService_.addCustomers( + vipGroup[0].id, + order.customer_id + ) + } + } +} + +export default AddCustomerToVipSubscriber +``` + +
+ + +--- + +## Marketing Automation + +In your commerce store, you may utilize marketing strategies that encourage customers to make purchases. + +For example, you may send a newsletter when new products are added to your store. In Medusa, this can be handled by listening to the [product events](../development/events/events-list.md#product-events), such as `product.created`, using a Subscriber, then sending an email to subscribed customers with tools like [SendGrid](../plugins/notifications/sendgrid.mdx) or [Mailchimp](../plugins/notifications/mailchimp.md). + +You can alternatively have a Scheduled Job that checks if the number of new products has exceeded a set threshold, then send out the newsletter. + + + +
+ +Example: Sending a newsletter email after adding ten products + + +Here’s an example of listening to the `product.created` event in a subscriber and send a newsletter if the condition is met: + +```ts title=src/subscribers/send-products-newsletter.ts +import { + CustomerService, + EventBusService, + NoteService, + OrderService, + ProductService, + StoreService, +} from "@medusajs/medusa" +import { + ProductSelector, +} from "@medusajs/medusa/dist/types/product" + +type InjectedDependencies = { + eventBusService: EventBusService + sendgridService: any + productService: ProductService + storeService: StoreService + customerService: CustomerService +} + +class SendProductsNewsletterSubscriber { + protected sendGridService_: any + protected productService_: ProductService + protected storeService_: StoreService + protected customerService_: CustomerService + + constructor({ + eventBusService, + sendgridService, + productService, + storeService, + customerService, + }: InjectedDependencies) { + this.productService_ = productService + this.sendGridService_ = sendgridService + this.storeService_ = storeService + this.customerService_ = customerService + eventBusService.subscribe( + "product.created", + this.handleProductCreated + ) + } + + handleProductCreated = async ({ id }) => { + // retrieve store to have access to last send date + const store = await this.storeService_.retrieve() + + const productFilters: ProductSelector = {} + if (store.metadata.last_send_date) { + productFilters.created_at = { + gt: new Date(store.metadata.last_send_date as string), + } + } + + const products = await this.productService_.list( + productFilters + ) + + if (products.length > 10) { + // get subscribed customers + const customers = await this.customerService_.list({ + metadata: { + is_subscribed: true, + }, + }) + this.sendGridService_.sendEmail({ + templateId: "product-newsletter", + from: "hello@medusajs.com", + to: customers.map((customer) => ({ + name: customer.first_name, + email: customer.email, + })), + data: { + // any data necessary for your template... + products, + }, + }) + + await this.storeService_.update({ + metadata: { + last_send_date: new Date(), + }, + }) + } + } +} + +export default SendProductsNewsletterSubscriber +``` + +
+ +--- + +## Automation Development Toolkit + +The use cases mentioned in this guide are just some examples of what commerce automation you can perform or implement with Medusa. Medusa provides the necessary development tools and mechanisms that allow you to automate tasks and manual work. + + diff --git a/www/docs/content/recipes/digital-products.mdx b/www/docs/content/recipes/digital-products.mdx new file mode 100644 index 0000000000..e6f6d1da1a --- /dev/null +++ b/www/docs/content/recipes/digital-products.mdx @@ -0,0 +1,1080 @@ +--- +addHowToData: true +--- + +import DocCardList from '@theme/DocCardList'; +import DocCard from '@theme/DocCard'; +import Icons from '@theme/Icon'; + +# Digital Products + +This document guides you through the different documentation resources that will help you build digital products with Medusa. + +## Overview + +A business can sell digital or downloadable products. The products are stored privately using a storage service, such as S3, or other storage mechanism. When the customer buys this type of product, an email is sent to them where they can download the product. + +Medusa doesn't have a built-in concept of a digital product since our focus is standardizing features and implementations, then offering the building blocks that enable you to build your use-case easily. + +You can create new entities that allow you to represent a digital product and link them to existing product-related entities. + +Medusa’s plugins also allows you to choose the services that play part in your digital products implementation. Whether you want to use S3, MinIO, or another service to store your digital products, all you have to do is install an existing plugin or create your own. The same applies for sending notifications, such as emails, to the customer using notification services. + +--- + +## Install a File Service + +A file service is used to handle storage functionalities in Medusa. This includes uploading, retrieving, and downloading files, among other features. For digital products, a file service is essential to implement the basic functionalities of digital products. + +The Medusa core defines an abstract file service that is extended by File Service plugins with the actual functionality. This allows you to choose the storage method that works best for your use case. + +By default, when you install a Medusa project, the Local file service plugin is installed. This plugin is useful for development, but it’s not recommended for production. + +Medusa also provides official file service plugins that you can use in production, such as the [S3 plugin](../plugins/file-service/s3.mdx) or the [MinIO plugin](../plugins/file-service/minio.md). You can also create your own file service, or browse the [Plugins Library](https://medusajs.com/plugins/?filters=Storage&categories=Storage) for plugins created by the community. + + + +--- + +## Install a Notification Service + +A notification service is used to handle sending notifications to users and customers. For example, when you want to send an order confirmation email or an email with reset-password instructions. + +For digital products, a notification service allows you to send the customer an email, or another form of notification, with a link to download the file they purchased. + +The Medusa core defines an abstract Notification service that is extended by Notification Service plugins with the actual functionality. This allows you to choose the notification method that best works for your use case. + +Medusa provides official notification plugins that you can use, such as the SendGrid plugin. You can also create your own notification service, or browse the [Plugins Library for plugins](https://medusajs.com/plugins/?filters=Storage&filters=Notification&categories=Storage&categories=Notification) created by the community. + + + +--- + +## Create Custom Entity + +An entity represents a table in the database. The Medusa core defines entities necessary for the commerce features it provides. You can also extend those entities or create your own. + +To represent a digital product, it’s recommended to create an entity that has a relation to the `ProductVariant` entity. The `ProductVariant` entity represents the saleable variant of a `Product`. + +For example, if you’re selling the Harry Potter movies, you would have a `Product` with the name “Harry Potter”, and for each movie in the series a `ProductVariant`. Each `ProductVariant` would be associated with the custom entity you create that represents the downloadable movie. + + + +
+ +Example: Create ProductMedia Entity + + +In this example, you’ll create a `ProductMedia` entity that is associated with the `ProductVariant` entity in a one-to-many relation. + +To do that, create the file `src/models/product-media.ts` with the following content: + +```ts title=src/models/product-media.ts +import { + BeforeInsert, + Column, + Entity, + JoinColumn, + ManyToOne, +} from "typeorm" +import { BaseEntity, ProductVariant } from "@medusajs/medusa" +import { generateEntityId } from "@medusajs/medusa/dist/utils" + +export enum MediaType { + MAIN = "main", + PREVIEW = "preview" +} + +@Entity() +export class ProductMedia extends BaseEntity { + @Column({ type: "varchar" }) + name: string + + @Column ({ type: "enum", enum: MediaType, default: "main" }) + type: MediaType + + @Column({ type: "varchar" }) + file_key: string + + @Column({ type: "varchar" }) + variant_id: string + + @BeforeInsert() + private beforeInsert(): void { + this.id = generateEntityId(this.id, "post") + } +} +``` + +The entity has the following attributes: + +- `name`: a string indicating the file name or a name entered by the merchant. +- `type`: an enum value indicating the type of file. If the file’s type is `main`, it means that this is the file that customers download when they purchase the product. If its type is `preview`, it means that the file is only used to preview the product variant to the customer. +- `file_key`: a string that indicates the downloadable file’s key. The key is retrieved by the installed file service, which is covered in the next step, and it’s used later if you want to get a downloadable link or delete the file. +- `variant_id`: a string indicating the ID of the product variant this file is associated with. + +Next, you need to create a migration script that reflects the changes on the database schema. + +To do that, run the following command to create a migration file: + +```bash +npx typeorm migration:create src/migrations/ProductMediaCreate +``` + +This will create a file in the `src/migrations` directory. The file’s name will be of the format `-ProductMediaCreate.ts`, where `` is the time this migration was created. + +In the class defined in the file, change the `up` and `down` method to the following: + + + +```ts +export class ProductMediaCreate1693901604934 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."product_media_type_enum" AS ENUM('main', 'preview')`) + await queryRunner.query(`CREATE TABLE "product_media" ("id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "name" character varying NOT NULL, "type" "public"."product_media_type_enum" NOT NULL DEFAULT 'main', "file_key" character varying NOT NULL, "variant_id" character varying NOT NULL, CONSTRAINT "PK_09d4639de8082a32aa27f3ac9a6" PRIMARY KEY ("id"))`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "product_media"`) + await queryRunner.query(`DROP TYPE "public"."product_media_type_enum"`) + } +} +``` + +To apply the migrations on your database and create the `product_media` table, run the following commands in the root directory of your Medusa backend: + +```bash npm2yarn +npm run build +npx medusa migrations run +``` + +
+ + +--- + +## Add Custom Endpoints + +After creating your entity, you need to create custom admin endpoints that allows you to access and manipulate that entity. The endpoints necessary vary based on your case, but generally speaking you’ll need endpoints to perform Create, Read, Update, and Delete (CRUD) functionalities. + +:::tip + +The Medusa backend provides the necessary endpoints for the actual file upload. So, you don’t need to create endpoints for that. + +::: + +You can also create custom storefront endpoints that allows you to show information related to the downloadable digital product if this information isn’t stored within the `Product` or `ProductVariant` entities. + +Creating an endpoint also requires creating a service, which is a class that typically holds utility methods for an entity. You can implement the CRUD functionalities as methods within the service, then access the service in an endpoint and use its methods. + + + +
+ +Example: List and Create Endpoints + + +This example showcases how you can create the list and create endpoints for digital products. These endpoints are chosen in particular as they’re used in later parts of this recipe. You may follow the same instructions to create other necessary endpoints. + +Before creating the endpoints, you’ll create the `ProductMediaService`. Create the file `src/services/product-media.ts` with the following content: + +```ts title=src/services/product-media.ts +import { + FindConfig, + Selector, + TransactionBaseService, + buildQuery, +} from "@medusajs/medusa" +import { ProductMedia } from "../models/product-media" +import { MedusaError } from "@medusajs/utils" + +class ProductMediaService extends TransactionBaseService { + + constructor(container) { + super(container) + } + + async listAndCount( + selector?: Selector, + config: FindConfig = { + skip: 0, + take: 20, + relations: [], + }): Promise<[ProductMedia[], number]> { + const productMediaRepo = this.activeManager_.getRepository( + ProductMedia + ) + + const query = buildQuery(selector, config) + + return productMediaRepo.findAndCount(query) + } + + async list( + selector?: Selector, + config: FindConfig = { + skip: 0, + take: 20, + relations: [], + }): Promise { + const [productMedias] = await this.listAndCount( + selector, + config + ) + + return productMedias + } + + async retrieve( + id: string, + config?: FindConfig + ): Promise { + const productMediaRepo = this.activeManager_.getRepository( + ProductMedia + ) + + const query = buildQuery({ + id, + }, config) + + const productMedia = await productMediaRepo.findOne(query) + + if (!productMedia) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "ProductMedia was not found" + ) + } + + return productMedia + } + + async create( + data: Pick< + ProductMedia, + "name" | "file_key" | "variant_id" | "type" + > + ): Promise { + return this.atomicPhase_(async (manager) => { + const productMediaRepo = manager.getRepository( + ProductMedia + ) + const productMedia = productMediaRepo.create(data) + const result = await productMediaRepo.save(productMedia) + + return result + }) + } + + async update( + id: string, + data: Omit, "id"> + ): Promise { + return await this.atomicPhase_(async (manager) => { + const productMediaRepo = manager.getRepository( + ProductMedia + ) + const productMedia = await this.retrieve(id) + + Object.assign(productMedia, data) + + return await productMediaRepo.save(productMedia) + }) + } + + async delete(id: string): Promise { + return await this.atomicPhase_(async (manager) => { + const productMediaRepo = manager.getRepository( + ProductMedia + ) + const productMedia = await this.retrieve(id) + + await productMediaRepo.remove([productMedia]) + }) + } +} + +export default ProductMediaService +``` + +This service implements the necessary methods to perform the basic CRUD operations. You can add any other method if necessary. + +You can now create the endpoints. To create the “create product media” endpoint, create the file `src/api/routes/admin/product-media/create.ts` with the following content: + +```ts title=src/api/routes/admin/product-media/create.ts +import { Request, Response } from "express" +import ProductMediaService + from "../../../../services/product-media" + +// create a product media for a variant +export default async (req: Request, res: Response) => { + // validation omitted for simplicity + const { + variant_id, + file_key, + type = "main", + name, + } = req.body + + const productMediaService = req.scope.resolve< + ProductMediaService + >("productMediaService") + const productMedia = await productMediaService.create({ + variant_id, + file_key, + type, + name, + }) + + res.json({ + product_media: productMedia, + }) +} +``` + +This creates an endpoint the accepts the necessary data to create a product media, then returns the created product media. + +Next, create the “list product media” endpoint by creating the file `src/api/routes/admin/product-media/list.ts` with the following content: + +```ts title=src/api/routes/admin/product-media/list.ts +import { Request, Response } from "express" +import ProductMediaService + from "../../../../services/product-media" +import { MediaType } from "../../../../models/product-media" + +// retrieve a list of product medias +export default async (req: Request, res: Response) => { + const productMediaService = req.scope.resolve< + ProductMediaService + >("productMediaService") + // omitting pagination for simplicity + const [productMedias, count] = await productMediaService + .listAndCount({ + type: MediaType.MAIN, + }, { + relations: ["variant", "variant.product"], + } + ) + + res.json({ + product_medias: productMedias, + count, + }) +} +``` + +This retrieves a list of product media records that have the type `main`. It also retrieves the relations `variant` and `variant.product` to access the product media’s variant and main product. + +To use these endpoints, you need to export them in a router that’s returned by `src/api/index.ts`. First, create the file `src/api/routes/admin/product-media/index.ts` that registers the product media endpoints you created: + +```ts title=src/api/routes/admin/product-media/index.ts +import { wrapHandler } from "@medusajs/utils" +import { Router } from "express" +import create from "./create" +import list from "./list" + +const router = Router() + +export default (adminRouter: Router) => { + adminRouter.use("/product-media", router) + + router.get("/", wrapHandler(list)) + router.post("/", wrapHandler(create)) +} +``` + +Then, create the file `src/api/routes/admin/index.ts` if it doesn’t already exist and add the following in it: + +```ts title=src/api/routes/admin/index.ts +import { Router } from "express" +import productMediaRoutes from "./product-media" + +// Initialize a custom router +const router = Router() + +export function attachAdminRoutes(adminRouter: Router) { + productMediaRoutes(adminRouter) + + // ... exisiting endpoints +} +``` + +In this file you import the function in `src/api/routes/admin/product-media/index.ts` that registers the necessary routes and pass it the admin router, which this file’s function accepts as a parameter. + +If the file `src/api/routes/admin/index.ts` wasn’t already created, make sure to import it in `src/api/index.ts` and use it to register the product media endpoints: + +```ts title=src/api/routes/admin/index.ts +import { Router } from "express" +import cors from "cors" +import bodyParser from "body-parser" +import { ConfigModule } from "@medusajs/medusa" +import { getConfigFile } from "medusa-core-utils" +import { attachStoreRoutes } from "./routes/store" +import { attachAdminRoutes } from "./routes/admin" + +export default (rootDirectory: string): Router | Router[] => { + // Read currently-loaded medusa config + const { configModule } = getConfigFile( + rootDirectory, + "medusa-config" + ) + const { projectConfig } = configModule + + // Set up our CORS options objects, based on config + const storeCorsOptions = { + origin: projectConfig.store_cors.split(","), + credentials: true, + } + + const adminCorsOptions = { + origin: projectConfig.admin_cors.split(","), + credentials: true, + } + + // Set up express router + const router = Router() + + // Set up root routes for store and admin endpoints, + // with appropriate CORS settings + router.use( + "/store", + cors(storeCorsOptions), + bodyParser.json() + ) + router.use( + "/admin", + cors(adminCorsOptions), + bodyParser.json() + ) + + // Set up routers for store and admin endpoints + const storeRouter = Router() + const adminRouter = Router() + + // Attach these routers to the root routes + router.use("/store", storeRouter) + router.use("/admin", adminRouter) + + // Attach custom routes to these routers + attachStoreRoutes(storeRouter) + attachAdminRoutes(adminRouter) + + return router +} +``` + +
+ + +--- + +## Customize Admin Dashboard + +The Medusa admin dashboard provides merchants with an easy-to-use interface to manage their store’s data and settings. It’s also extendable to add widgets, pages, and setting pages relevant to your use case. + +To add an interface that allows the admin user to upload digital products, you can create custom widgets or pages that uses the routes you created. For the actual file upload, you can use the [Protected Files Upload endpoint](https://docs.medusajs.com/api/admin#uploads_postuploadsprotected). + + + +
+ +Example: Digital Products Page in Admin + + +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. + +Before starting off, make sure to install the necessary dependencies in your Medusa backend project: + +```bash npm2yarn +npm install medusa-react @tanstack/react-query @medusajs/ui +``` + +This installs the necessary packages to use the Medusa React client and the [Medusa UI package](https://docs.medusajs.com/ui). + +You also need to create types for the expected requests and responses of the endpoints you created. This is helpful when using Medusa React’s custom hooks. To do that, create the file `src/types/product-media.ts` with the following content: + +```ts title=src/types/product-media.ts +import { + MediaType, + ProductMedia, +} from "../models/product-media" + +export type ListProductMediasRequest = { + // no expected parameters +}; + +export type ListProductMediasResponse = { + product_medias: ProductMedia[] + count: number +}; + +export type CreateProductMediaRequest = { + variant_id: string + name: string + file_key: string + type?: MediaType +}; + +export type CreateProductMediaResponse = { + product_media: ProductMedia +}; +``` + +You can now create your admin UI route. To do that, create the file `src/admin/routes/product-media/page.tsx` with the following content: + +```tsx title=src/admin/routes/product-media/page.tsx +import { RouteConfig } from "@medusajs/admin" +import { DocumentText } from "@medusajs/icons" +import { useAdminCustomQuery } from "medusa-react" +import { + ListProductMediasRequest, + ListProductMediasResponse, +} from "../../../types/product-media" +import { + Button, + Container, + Drawer, + Heading, + Table, +} from "@medusajs/ui" +import { Link } from "react-router-dom" +import { RouteProps } from "@medusajs/admin-ui" +import ProductMediaCreateForm + from "../../components/product-media/CreateForm" + +const ProductMediaListPage = (props: RouteProps) => { + const { data, isLoading } = useAdminCustomQuery< + ListProductMediasRequest, + ListProductMediasResponse + >( + "/product-media", + ["product-media"] + ) + + return ( + +
+ Digital Products + + + + + + + + Create Digital Product + + + + + + + +
+ {isLoading &&
Loading...
} + {data && !data.product_medias.length && ( +
No Digital Products
+ )} + {data && data.product_medias.length > 0 && ( + + + + Product + + Product Variant + + File Key + Action + + + + {data.product_medias.map((product_media) => ( + + + {product_media.variant.product.title} + + + {product_media.variant.title} + + + {product_media.file_key} + + + + View Product + + + + ))} + +
+ )} +
+ ) +} + +export const config: RouteConfig = { + link: { + label: "Digital Products", + icon: DocumentText, + }, +} + +export default ProductMediaListPage +``` + +This UI route will show under the sidebar with the label “Digital Products”. In the page, you use the `useAdminCustomQuery` hook imported from `medusa-react` to send a request to your custom “list digital products” endpoint. + +In the page, you’ll show the list of digital products in a table, if there are any. You’ll also show a button that opens a drawer to the side of the page. + +In the drawer, you show the Create Digital Product form. To create this form, create the file `src/admin/components/product-media/CreateForm/index.tsx` with the following content: + +```tsx title=src/admin/components/product-media/CreateForm/index.tsx +import { useState } from "react" +import { MediaType } from "../../../../models/product-media" +import { + useAdminCreateProduct, + useAdminCustomPost, + useAdminUploadProtectedFile, +} from "medusa-react" +import { + CreateProductMediaRequest, + CreateProductMediaResponse, +} from "../../../../types/product-media" +import { + Button, + Container, + Input, + Label, + Select, +} from "@medusajs/ui" +import { RouteProps } from "@medusajs/admin-ui" +import { useNavigate } from "react-router-dom" + +const ProductMediaCreateForm = ({ + notify, +}: RouteProps) => { + const [productName, setProductName] = useState("") + const [ + productVariantName, + setProductVariantName, + ] = useState("") + const [name, setName] = useState("") + const [type, setType] = useState("main") + const [file, setFile] = useState() + + const createProduct = useAdminCreateProduct() + const uploadFile = useAdminUploadProtectedFile() + const { + mutate: createDigitalProduct, + isLoading, + } = useAdminCustomPost< + CreateProductMediaRequest, + CreateProductMediaResponse + >( + "/product-media", + ["product-media"] + ) + + const navigate = useNavigate() + + const handleSubmit = ( + e: React.FormEvent + ) => { + e.preventDefault() + + createProduct.mutate({ + title: productName, + is_giftcard: false, + discountable: false, + options: [ + { + title: "Digital Product", + }, + ], + variants: [ + { + title: productVariantName, + options: [ + { + value: name, // can also be the file name + }, + ], + // for simplicity, prices are omitted from form. + // Those can be edited from the product's page. + prices: [], + }, + ], + }, { + onSuccess: ({ product }) => { + // upload file + uploadFile.mutate(file, { + onSuccess: ({ uploads }) => { + if (!("key" in uploads[0])) { + return + } + // create the digital product + createDigitalProduct({ + variant_id: product.variants[0].id, + name, + file_key: uploads[0].key as string, + type: type as MediaType, + }, { + onSuccess: () => { + notify.success( + "Success", + "Digital Product Created Successfully" + ) + navigate("/a/product-media") + }, + }) + }, + }) + }, + }) + } + + return ( + +
+
+ + setProductName(e.target.value)} + /> +
+
+ + + setProductVariantName(e.target.value) + } + /> +
+
+ + setName(e.target.value)} + /> +
+
+ + +
+
+ + setFile(e.target.files[0])} + /> +
+ +
+
+ ) +} + +export default ProductMediaCreateForm +``` + +In this component, you create a form that accepts basic information needed to create the digital product. This form only accepts one file for one variant for simplicity purposes. You can expand on this based on your use case. + +Notice that an alternative approach would be to inject a widget to the Product Details page and allow users to upload the files from there. It depends on whether you’re only supporting Digital Products or you want the distinction between them, as done here. + +When the user submits the form, you first create a product with a variant. Then, you upload the file using the [Upload Protected File endpoint](https://docs.medusajs.com/api/admin#uploads_postuploadsprotected). Finally, you create the digital product using the custom endpoint you created. + +The product’s details can still be edited from the same Products interface, similar to regular products. You can edit its price, add more variants, and more. + +To test it out, build changes and run the `develop` command: + +```bash npm2yarn +npm run build +npx medusa develop +``` + +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. + +
+ +--- + +## Deliver Digital Products to the Customer + +When a customer purchases a digital product, they should receive a link to download it. + +To implement that, you first need to listen to the `order.placed` event using a Subscriber. Then, in the handler method of the subscriber, you check for the digital products in the order, and obtain the download URLs using the file service’s `getPresignedDownloadUrl` method. + +Finally, you can send a notification, such as an email, to the customer using the Notification Service of your choice. That notification would hold the download links to the products they purchased. + + + +
+ +Example: Using SendGrid + + +Here’s an example of a subscriber that retrieves the download links and sends them to the customer using the SendGrid plugin: + +```ts title=src/subscribers/handle-order.ts +import { + AbstractFileService, + EventBusService, + OrderService, +} from "@medusajs/medusa" + +type InjectedDependencies = { + eventBusService: EventBusService + orderService: OrderService + sendgridService: any + fileService: AbstractFileService +} + +class HandleOrderSubscribers { + protected readonly orderService_: OrderService + protected readonly sendgridService_: any + protected readonly fileService_: AbstractFileService + + constructor({ + eventBusService, + orderService, + sendgridService, + fileService, + }: InjectedDependencies) { + this.orderService_ = orderService + this.sendgridService_ = sendgridService + this.fileService_ = fileService + eventBusService.subscribe( + "order.placed", + this.handleOrderPlaced + ) + } + + handleOrderPlaced = async ( + data: Record + ) => { + const order = await this.orderService_.retrieve(data.id, { + relations: [ + "items", + "items.variant", + "items.variant.product_medias", + ], + }) + + // find product medias in the order + const urls = [] + for (const item of order.items) { + if (!item.variant.product_medias.length) { + return + } + + await Promise.all([ + item.variant.product_medias.forEach( + async (productMedia) => { + // get the download URL from the file service + const downloadUrl = await + this.fileService_.getPresignedDownloadUrl({ + fileKey: productMedia.file_key, + isPrivate: true, + }) + + urls.push(downloadUrl) + }), + ]) + } + + if (!urls.length) { + return + } + + this.sendgridService_.sendEmail({ + templateId: "digital-download", + from: "hello@medusajs.com", + to: order.email, + data: { + // any data necessary for your template... + digital_download_urls: urls, + }, + }) + } +} + +export default HandleOrderSubscribers +``` + +Notice that regardless of what file service you have installed, you can access it using dependency injection under the name `fileService`. + +The `handleOrderPlaced` method retrieves the order, loops over its items to find digital products and retrieve their download links, then uses SendGrid to send the email, passing it the urls as a data payload. You can customize the sent data based on your SendGrid template and your use case. + +
+ + +--- + +## Additional Development + +You can find other resources for your dit development in the [Medusa Development section](../development/overview.mdx) of this documentation. diff --git a/www/docs/content/recipes/index.mdx b/www/docs/content/recipes/index.mdx index 10f2c6d6d9..0df9bb56bc 100644 --- a/www/docs/content/recipes/index.mdx +++ b/www/docs/content/recipes/index.mdx @@ -1,5 +1,4 @@ import DocCardList from '@theme/DocCardList'; -import DocCard from '@theme/DocCard'; import Icons from '@theme/Icon'; import Feedback from '@site/src/components/Feedback'; import LearningPath from '@site/src/components/LearningPath'; @@ -31,75 +30,11 @@ Follow this recipe if you want to use Medusa for an ecommerce store. This recipe --- -## Recipe: Build a Marketplace +## Use-Case Based Recipes -Follow this guide if you want to build a Marketplace with Medusa. +Explore available recipes for different commerce use cases. - - ---- - -## Recipe: Build Subscription Purchases - -Follow this guide if you want to implement subscription-based purchases with Medusa. - - - ---- - -## Recipe: Build B2B Store - -Follow this guide if you want to build a B2B/Wholesale store. - - - - - ---- - -## Recipe: Build Multi-Region Store - -Learn how you can build a multi-region store with Medusa. - - + + +--- + +## Integrate with Marketplaces and Social Commerce + +Businesses are no longer bound to sell in their own stores. They can reach their customers through their different shopping channels. + +One example is marketplaces like Amazon. Customers searching through Amazon to find products are inadvertently searching through a huge number of third-party stores connected to Amazon’s marketplace. + +With Medusa, you can use the Sales Channels feature that allows you to set different product availability across sales channels. You can then integrate with Amazon’s seller program and use their APIs to push your products on Amazon. This integration can be achieved through a custom plugin that you create and install on your Medusa backend. + +Another channel that attracts customer sales is social media. Customers browsing a social media app can now find products and buy them. With an approach similar to what was mentioned above, you can create a plugin in your Medusa backend that integrates with social media apps to show your products and sell them to customers on the app. + + + +--- + +## Optimize Customer Experience + +Each business has a unique brand and wants to provide customers with the best journey and experience. With Medusa’s modular architecture, businesses can focus on bringing their brands to life without any restrictions. + +For your storefronts, you can implement the customer journey and design that leads to the best customer experience. The backend does not impose any restrictions on how the frontend is built. + +Medusa’s architecture also makes it easier to integrate any third-party services necessary to provide a better customer experience with a plugin. This plugin’s functionality can be accessible to the storefronts connected to the Medusa backend. + +For example, if you’ve integrated a search plugin such as Algolia in the Medusa backend, the storefronts can utilize it through the Search Products endpoint. Another example is installing the Stripe payment plugin on the backend, and utilizing it in your storefronts to make payments before placing an order. + + + +--- + +## Additional Development + +You can find other resources for your omnichannel development in the [Medusa Development section](../development/overview.mdx) of this documentation. diff --git a/www/docs/content/recipes/oms.mdx b/www/docs/content/recipes/oms.mdx new file mode 100644 index 0000000000..04838acd39 --- /dev/null +++ b/www/docs/content/recipes/oms.mdx @@ -0,0 +1,237 @@ +--- +addHowToData: true +--- + +import DocCardList from '@theme/DocCardList'; +import DocCard from '@theme/DocCard'; +import Icons from '@theme/Icon'; + +# Order Management System (OMS) + +This document guides you through the different features and resources available in Medusa to use it as an order management system (OMS). + +## Overview + +An order management system (OMS) is a system that provides features to track and manage orders and typically integrates with third-party services to handle payment, fulfillment, and more. Businesses use an OMS as part of a bigger commerce ecosystem that uses different services to achieve the business use case. + +Medusa can be used within a larger ecosystem as an OMS. Its modular architecture and commerce features make it easy for developers to integrate it with other services while utilizing powerful OMS features, including returns, exchanges, and order edits. + +--- + +## Source Orders into Medusa + +Since an OMS is part of a larger ecosystem, your orders will likely come from different sources or channels. All these channels must ultimately direct their order to the OMS. This can be done in different ways in Medusa depending on your use case. + +The basic way would be to use the REST APIs to create an order in Medusa. You can use the [Store REST APIs](https://docs.medusajs.com/api/store#carts), which are useful if the order is placed directly by the customer such as through a storefront. Alternatively, you can use the [Draft Orders](https://docs.medusajs.com/api/admin#draft-orders) feature that allows you to create the order without customer interference. This method can be helpful if you’re creating the order through an automated script or from a channel managed by a store operator, such as a [POS](./pos.mdx). + +If your use case is more advanced or complex, you can create a custom endpoint that gives you more control over how you create the order. For example, you can create a custom endpoint that allows creating or inserting a batch of orders. + +You can also create a Scheduled Job to import orders into Medusa from an external service or system at a defined interval. + + + +--- + +## Route Orders to Third-party Fulfillment Services + +In your ecosystem, you have different third-party services and tools that handle the fulfillment of orders. The OMS integrates with these services to ensure they’re used when fulfillment events occur. + +With Medusa, you can integrate third-party fulfillment services either by creating a fulfillment provider or installing an existing fulfillment plugin. The fulfillment provider must implement different methods that are used when fulfillment actions are taken on the order. + +For example, when an admin creates a fulfillment on an order, the method defined in the fulfillment provider associated with the order is executed. In that method, you can create the necessary implementation that handles creating the fulfillment in the third-party service. + +In addition, Medusa has an event bus that allows you to listen to events and perform an action asynchronously when that event is triggered. So, you can listen to fulfillment-related events, such as the `order.fulfillment_created` event, and handle that event in the subscribed method when it’s triggered. + + + +--- + +## Process Payment with Third-Party Providers + +Similar to fulfillment services, you’ll also have payment providers that handle processing payments. Your OMS integrates with these providers to handle payment-related actions. + +Medusa allows you to integrate third-party payment providers by creating a payment processor. The payment processor must implement different methods that are used when the order’s payment is being processed, such as when it’s captured or refunded. + +Alternatively, you can use existing payment plugins. Medusa provides official payment plugins for [Stripe](../plugins/payment/stripe.mdx), [PayPal](../plugins/payment/paypal.md), and [Klarna](../plugins/payment/klarna.md). + + + +--- + +## Handle Returns, Exchanges, and Edits + +Medusa provides advanced order features that make it an ideal OMS choice and allow you to provide a great customer experience. + +Items in an order can be returned or exchanged. The return or exchange can either be requested by the customer or created by an admin. Once created, they reflect changes in the order’s total and inventory and allow the admin to issue a refund to the customer as well if necessary. In the case of exchanges, the fulfillment provider will be used to fulfill the new item sent to the customer. + +Orders can also be edited to add, update, or delete items in an order. Additional payment can be authorized, or a refund can be issued, if necessary. Once an order edit is confirmed, the changes are reflected on the order’s totals and inventory. + +These features can be used through the [Admin](https://docs.medusajs.com/api/admin) and [Store REST APIs](https://docs.medusajs.com/api/store). You can also perform these actions in your custom endpoints or services using the [core services](../references/services/index.md) and their methods. + +Once these actions occur, events, such as `order.return_requested` are triggered by Medusa’s event bus. So, you can listen to different events to perform asynchronous actions, for example, to communicate with third-party services or tools. + + + +--- + +## Track Inventory Across Sales Channels + +An OMS typically connects to more than one sales channel, such as one or more storefronts and a POS. It’s important to keep track of your inventory across these sales channels. + +Medusa’s multi-warehouse and sales channel features allow you to track inventory levels across different stock locations, which are tied to sales channels. When an order is placed, an item in a stock location, based on the order’s sales channel, is reserved. Once the item is fulfilled, the reserved quantity is deducted from the item’s inventory quantity. + +The inventory is also changed when an item is returned, exchanged, or when an order is edited. + +Medusa’s inventory and stock location modules that power its multi-warehouse features can also be completely replaced with a custom implementation. This allows you to take control of how inventory functionalities are implemented in Medusa. + +In addition, you can integrate third-party inventory systems by creating or installing a plugin. Medusa provides an official [Brightpearl](https://github.com/medusajs/medusa/tree/develop/packages/medusa-plugin-brightpearl) plugin that syncs orders and inventory with Brightpearl. + + + +--- + +## Additional Development and Commerce Features + +Medusa provides more commerce features that you can learn about in the [Commerce Modules](../modules/overview.mdx) section of this documentation. + +Medusa is also completely customizable, and you can learn more about customizing it in the [Medusa Development](../development/overview.mdx) section of this documentation. diff --git a/www/docs/content/recipes/personalized-products.mdx b/www/docs/content/recipes/personalized-products.mdx new file mode 100644 index 0000000000..2d27171d33 --- /dev/null +++ b/www/docs/content/recipes/personalized-products.mdx @@ -0,0 +1,166 @@ +--- +addHowToData: true +--- + +import DocCardList from '@theme/DocCardList'; +import Icons from '@theme/Icon'; + +# Personalized Products + +This document guides you through the different documentation resources that will help you build personalized products with Medusa. + +## Overview + +Commerce stores that provide personalized products allow customers to pick and choose how the product looks or what features it includes. For example, they can upload an image to print on a shirt or provide a message to include in a letter. + +Medusa’s customizable architecture allows you to customize its entities or create your own to implement and store personalized products. Also, as the Medusa backend is headless, you have freedom in how you choose to implement the storefront. This is essential for ecommerce stores that provide personalized products, as you typically build a unique experience around your products. + +--- + +## Store Personalized Data + +Most of the entities in Medusa’s core include a `metadata` attribute. This attribute is helpful for storing custom data in the core entities. + +The `Product` entity represents the main product, whereas the `ProductVariant` is the different saleable options of that product. For example, a shirt is a `Product`, and each different color of the shirt is the `ProductVariant`. The `LineItem` entity is the product variant added to the cart. + +So, you can use the `metadata` attribute of the `LineItem` entity to store the customer’s personalization. Optionally, you can use the `metadata` attribute in the `Product` or `ProductVariant` to store the expected format of personalized data. This depends on your use case and how basic or complex it is. + +For example, if you’re asking customers to enter a message to put in a letter they’re purchasing, you can use the `metadata` attribute of the `LineItem` entity to set the personalized information entered by the customer. + +In more complex cases, you can extend entities from the core, such as the `Product` entity, to add more attributes. You can also create new custom entities to hold your personalized data and logic. + + + +--- + +## Build a Custom Storefront + +Medusa provides a headless backend that can be accessed through REST APIs. So, there are no restrictions in what language or framework you use to build the storefront, or what design or experience you provide to customers. + +You can build a unique experience around your products that focuses on the customer’s personalization capabilities. + +Medusa provides a Next.js storefront starter with basic ecommerce functionalities that can be used and modified. You can also build your own storefront by using Medusa’s client libraries or REST APIs to communicate with the backend from the storefront. + + + +--- + +## Pass Personalized Data to the Order + +If you followed the basic approach of using the `metadata` attribute to store the personalized data, you can pass the personalization data when you add an item to the cart in the storefront using the [Add a Line Item](https://docs.medusajs.com/api/store#carts_postcartscartlineitems) endpoint. This endpoint accepts a `metadata` request body parameter that will be stored in the created line item’s `metadata` attribute. + +In the case that you’ve created a custom entity or extended a core entity, you can create a custom endpoint that handles saving the personalization data. You can then call that endpoint from the storefront before or after adding the item to the cart. + + + +--- + +## Access Personalized Data in the Order + +If you stored the personalized data in the `metadata` of the line items, those same line items are associated with the placed order. So, by expanding the `items` relation on the `Order` entity, you can retrieve the `metadata` attribute of the line items. + +In the case that you’ve created a custom entity or extended a core entity, you can create a custom endpoint that handles retrieving the personalization data. If the entity you’ve created or customized is associated with the Order entity, you can alternatively expand it similarly to the `items` relation. + +If you want to show the personalized data in the Medusa admin, you can extend the Medusa admin to add a widget, a UI route, or a setting page, and show the personalized data. + + + +--- + +## Additional Development + +You can find other resources for your personalized products development in the [Medusa Development section](../development/overview.mdx) of this documentation. diff --git a/www/docs/content/recipes/pos.mdx b/www/docs/content/recipes/pos.mdx new file mode 100644 index 0000000000..fa6cfff339 --- /dev/null +++ b/www/docs/content/recipes/pos.mdx @@ -0,0 +1,305 @@ +--- +addHowToData: true +--- + +import DocCardList from '@theme/DocCardList'; +import Icons from '@theme/Icon'; + +# POS + +This document guides you through the different features and resources available in Medusa to create a point-of-sale (POS) system. + +## Overview + +A POS system can be used in a business’s retail or offline stores to access and manage products and their inventories, and place orders for customers. Based on your use case, you can provide more features that can improve the customer experience. For example, customer discounts or Return Merchandise Authorization (RMA) features. + +Medusa’s architecture, commerce features, and customization capabilities allow you to build a POS system without any limitations or restrictions. + +Medusa’s headless backend facilitates creating any type of frontend that can communicate with the backend, including a POS system. Also, Medusa’s commerce features, including mutli-warehouse, sales channels, and order management features, can power your POS system to provide essential features for both store operators and customers making their purchase. + +:::tip + +Recommended read: [How Tekla built a POS system with Medusa](https://medusajs.com/blog/tekla-agilo-pos-case/). + +::: + +--- + +## Freedom in Choosing Your POS Tech Stack + +When you decide to build a POS system, you have to make an important choice of which programming framework, language, or tool you want to use. In most cases, this can be interdependent on the commerce engine, as it can limit your choices. + +Medusa’s modular architecture removes any restrictions you may have while making this choice. Regardless of what you choose, any client or front end can connect to the Medusa backend using its headless REST APIs. So, when building the POS system, you’ll be interacting with the [Admin REST APIs](https://docs.medusajs.com/api/admin). + +For example, you can use [Expo](https://expo.dev/) with [React Native](https://reactnative.dev/) to build a cross-platform POS app. In this case, you can also utilize [Medusa’s JavaScript client](../js-client/overview.md) to make it even easier to send requests to the backend. + + + +--- + +## Integrate a Barcode Scanner + +An essential feature a POS system needs is the capability to search and find products by scanning their barcode. You can then check their inventory information or add them to the customer’s order. + +Medusa’s [Product Variant entity](../references/entities/classes/ProductVariant.md), which represents a saleable stock item, already includes the necessary attributes to implement this integration, mainly the `barcode` attribute. Other notable attributes include `ean`, `upc`, and `hs_code`, among others. + +To search through product variants by their barcode, you can create a custom endpoint and use it within your POS. The endpoint can receive the item’s barcode, and return all product variants having that barcode. + +You can also create another custom endpoint that allows you to scan products in your store by their barcode and automatically add them to your backend. This eliminates the need to enter your product details manually. + + + +
+ +Example: Search Products By Barcode Endpoint + + +Here’s an example of creating a custom endpoint that searches product variants by a barcode: + +```ts title=src/api/index.ts +import { Request, Response, Router } from "express" +import cors from "cors" +import { + ConfigModule, + errorHandler, + ProductVariantService, + wrapHandler, +} from "@medusajs/medusa" +import { getConfigFile } from "medusa-core-utils" +import { MedusaError } from "@medusajs/utils" + +export default (rootDirectory: string): Router | Router[] => { + // Read currently-loaded medusa config + const { configModule } = getConfigFile( + rootDirectory, + "medusa-config" + ) + const { projectConfig } = configModule + + // Set up our CORS options objects, based on config + const storeCorsOptions = { + origin: projectConfig.store_cors.split(","), + credentials: true, + } + + // Set up express router + const router = Router() + + router.get( + "/store/search-barcode", + cors(storeCorsOptions), + wrapHandler( + async (req: Request, res: Response) => { + const barcode = (req.query.barcode as string) || "" + if (!barcode) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Barcode is required" + ) + } + // get product service + const productVariantService = req.scope.resolve< + ProductVariantService + >("productVariantService") + + // retrieve product variants by barcode + const productVariants = await productVariantService + .list({ + barcode, + }) + + res.json({ + product_variants: productVariants, + }) + } + ) + ) + + router.use(errorHandler()) + + return router +} +``` + +
+ +--- + +## Access Accurate Inventory Details + +As you manage an online and offline store, it’s important to make a separation between inventory quantity across different locations. For example, when an online order is made, the change in the inventory quantity of the order items should be made from the warehouse’s inventory, and not from the retail store’s. + +Medusa’s multi-warehouse features allow you to manage the inventory items and their availability across locations and sales channels. You can create a sales channel for your online store and a sales channel for your POS system, then manage the inventory quantity of product variants in each of those sales channels. You can also have different sales channels for different retail stores that use the POS system. + +When a customer browses your online store and wants to make a purchase, they can only buy items that are available in stock in the location associated with the online store’s channel. + +In the POS system, you can use the admin [stock location](https://docs.medusajs.com/api/admin#stock-locations) and [inventory item APIs](https://docs.medusajs.com/api/admin#inventory-items) to check whether an item is available in inventory or not. This helps store operators provide better and quicker customer service. + +This also opens the door for other business opportunities, such as an endless aisle experience. If a product isn’t available in-store but is available in other warehouses, you can allow a customer to purchase that item in-store and have it delivered to their address. + + + +--- + +## Build an Omni-channel Customer Experience + +A customer making a purchase in-store should have the same benefits as an online customer. You can find their customer details, if they’re registered in the ecommerce store, and provide them with applicable discounts if necessary. + +Using Medusa’s Customer APIs and features, you can easily query customers and place an order under their account. The customer can then view their order details on their profile as if they had placed the order online. + +In addition, using Medusa’s dynamic discounts feature, store operators can create discounts on the fly for customers using the POS system and apply them to their orders. This improves customer experience and builds customer loyalty. + +You can further customize your setup to provide a better customer experience. For example, you can create a rewards system or loyalty points that your customers can benefit from when they make purchases either through the POS system or the online store. + + + +--- + +## Accept Payment, Place Order, and Use RMA Features + +The first element to placing an order is accepting the customer’s payment. This is handled differently based on the payment methods you want to provide. + +Medusa provides a [manual payment processor](https://github.com/medusajs/medusa/tree/develop/packages/medusa-payment-manual) with a minimal implementation that assumes merchants handle all payment operations manually. Alternatively, you can integrate payment processors that allow you to accept in-store payments, such as [Stripe Terminal](https://stripe.com/terminal), by creating a payment processor. + +To place an order in the POS system, you can use the [Draft Order APIs](https://docs.medusajs.com/api/admin#draft-orders). This would allow the store operator to place the order under the customer’s account if they’re registered, and add all the necessary details related to discounts and more, similar to placing an order through the online store. + +Using the Medusa admin dashboard, merchants can view all orders coming from different sales channels. This keeps logistics and order handling consistent throughout orders. This also allows customers who place their order in-store to benefit from features like returns and exchanges or swaps that are already available for online store orders. + + + +--- + +## Additional Development + +You can find other resources for your POS development in the [Medusa Development section](../development/overview.mdx) of this documentation. diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index 4260974c8d..5bbb2aed42 100644 --- a/www/docs/sidebars.js +++ b/www/docs/sidebars.js @@ -51,21 +51,81 @@ module.exports = { type: "doc", id: "recipes/marketplace", label: "Marketplace", + customProps: { + iconName: "building-storefront", + }, }, { type: "doc", id: "recipes/subscriptions", label: "Subscriptions", + customProps: { + iconName: "credit-card-solid", + }, + }, + { + type: "doc", + id: "recipes/commerce-automation", + label: "Commerce Automation", + customProps: { + iconName: "clock-solid-mini", + }, + }, + { + type: "doc", + id: "recipes/oms", + label: "Order Management System", + customProps: { + iconName: "check-circle-solid", + }, + }, + { + type: "doc", + id: "recipes/pos", + label: "POS", + customProps: { + iconName: "computer-desktop-solid", + }, + }, + { + type: "doc", + id: "recipes/digital-products", + label: "Digital Products", + customProps: { + iconName: "photo-solid", + }, + }, + { + type: "doc", + id: "recipes/personalized-products", + label: "Personalized Products", + customProps: { + iconName: "swatch-solid", + }, }, { type: "doc", id: "recipes/b2b", label: "B2B / Wholesale", + customProps: { + iconName: "building-solid", + }, }, { type: "doc", id: "recipes/multi-region", label: "Multi-Region Store", + customProps: { + iconName: "globe-europe-solid", + }, + }, + { + type: "doc", + id: "recipes/omnichannel", + label: "Omnichannel Store", + customProps: { + iconName: "channels-solid", + }, }, ], }, @@ -2408,6 +2468,16 @@ module.exports = { "Learn how to integrate ipstack to access the user's region.", }, }, + { + type: "doc", + id: "plugins/other/restock-notifications", + label: "Restock Notifications", + customProps: { + iconName: "bolt-solid", + description: + "Learn how to integrate restock notifications with the Medusa backend.", + }, + }, { type: "doc", id: "plugins/other/discount-generator", diff --git a/www/docs/src/theme/Icon/PhotoSolid/index.tsx b/www/docs/src/theme/Icon/PhotoSolid/index.tsx new file mode 100644 index 0000000000..57871093a7 --- /dev/null +++ b/www/docs/src/theme/Icon/PhotoSolid/index.tsx @@ -0,0 +1,30 @@ +import React from "react" +import { IconProps } from ".." + +const IconPhotoSolid: React.FC = ({ + iconColorClassName, + ...props +}) => { + return ( + + + + ) +} + +export default IconPhotoSolid diff --git a/www/docs/src/theme/Icon/PlusMini/index.tsx b/www/docs/src/theme/Icon/PlusMini/index.tsx index 7dd4790310..a7a52d53a5 100644 --- a/www/docs/src/theme/Icon/PlusMini/index.tsx +++ b/www/docs/src/theme/Icon/PlusMini/index.tsx @@ -1,7 +1,10 @@ import React from "react" import { IconProps } from ".." -const IconPlusMini = ({ iconColorClassName, ...props }: IconProps) => { +const IconPlusMini: React.FC = ({ + iconColorClassName, + ...props +}) => { return (