docs: migrate guides to TSDoc references (#6100)

This commit is contained in:
Shahed Nasser
2024-01-22 18:38:35 +01:00
committed by GitHub
parent 85dad169bb
commit 4792c55226
980 changed files with 195537 additions and 160619 deletions
+300 -20
View File
@@ -4,62 +4,342 @@ import {
FileServiceGetUploadStreamResult,
FileServiceUploadResult,
GetUploadedFileType,
MedusaContainer,
UploadStreamDescriptorType,
} from "@medusajs/types"
/**
* ## Overview
*
* A file service class is defined in a TypeScript or JavaScript file thats created in the `src/services` directory.
* The class must extend the `AbstractFileService` class imported from the `@medusajs/medusa` package.
*
* Based on services naming conventions, the files name should be the slug version of the file services name
* without `service`, and the classs name should be the pascal case of the file services name following by `Service`.
*
* For example, create the file `src/services/local-file.ts` with the following content:
*
* ```ts title="src/services/local-file.ts"
* import { AbstractFileService } from "@medusajs/medusa"
* import {
* DeleteFileType,
* FileServiceGetUploadStreamResult,
* FileServiceUploadResult,
* GetUploadedFileType,
* UploadStreamDescriptorType,
* } from "@medusajs/types"
*
* class LocalFileService extends AbstractFileService {
* async upload(
* fileData: Express.Multer.File
* ): Promise<FileServiceUploadResult> {
* throw new Error("Method not implemented.")
* }
* async uploadProtected(
* fileData: Express.Multer.File
* ): Promise<FileServiceUploadResult> {
* throw new Error("Method not implemented.")
* }
* async delete(fileData: DeleteFileType): Promise<void> {
* throw new Error("Method not implemented.")
* }
* async getUploadStreamDescriptor(
* fileData: UploadStreamDescriptorType
* ): Promise<FileServiceGetUploadStreamResult> {
* throw new Error("Method not implemented.")
* }
* async getDownloadStream(
* fileData: GetUploadedFileType
* ): Promise<NodeJS.ReadableStream> {
* throw new Error("Method not implemented.")
* }
* async getPresignedDownloadUrl(
* fileData: GetUploadedFileType
* ): Promise<string> {
* throw new Error("Method not implemented.")
* }
* }
*
* export default LocalFileService
* ```
*
* :::note[Multer Typing]
*
* The examples implement a file service supporting local uploads.
*
* If youre using TypeScript and you're following along with the implementation,
* you should install the Multer types package in the root of your Medusa backend to resolve errors within your file service types:
*
* ```bash npm2yarn
* npm install @types/multer
* ```
*
* :::
*
* ---
*/
export interface IFileService extends TransactionBaseService {
/**
* upload file to fileservice
* @param file Multer file from express multipart/form-data
* */
* This method is used to upload a file to the Medusa backend.
*
* @param {Express.Multer.File} file - A [multer file object](http://expressjs.com/en/resources/middleware/multer.html#file-information).
* The file is uploaded to a temporary directory by default. Among the files details, you can access the files path in the `path` property of the file object.
* @returns {Promise<FileServiceUploadResult>} The details of the upload's result.
*
* @example
* ```ts
* class LocalFileService extends AbstractFileService {
* // ...
* async upload(
* fileData: Express.Multer.File
* ): Promise<FileServiceUploadResult> {
* const filePath =
* `${this.publicPath}/${fileData.originalname}`
* fs.copyFileSync(fileData.path, filePath)
* return {
* url: `${this.serverUrl}/${filePath}`,
* key: filePath,
* }
* }
* // ...
* }
* ```
*
* :::tip
*
* This example does not account for duplicate names to maintain simplicity in this guide. So, an uploaded file can replace another existing file that has the same name.
*
* :::
*/
upload(file: Express.Multer.File): Promise<FileServiceUploadResult>
/**
* upload private file to fileservice
* @param file Multer file from express multipart/form-data
* */
* This method is used to upload a file to the Medusa backend, but to a protected storage. Typically, this would be used to store files that
* shouldnt be accessible by using the files URL or should only be accessible by authenticated users. For example, exported or imported
* CSV files.
*
* @param {Express.Multer.File} file - A [multer file object](http://expressjs.com/en/resources/middleware/multer.html#file-information).
* The file is uploaded to a temporary directory by default. Among the files details, you can access the files path in the `path` property of the file object.
* @returns {Promise<FileServiceUploadResult>} The details of the upload's result.
*
* @example
* ```ts
* class LocalFileService extends AbstractFileService {
* // ...
* async uploadProtected(
* fileData: Express.Multer.File
* ): Promise<FileServiceUploadResult> {
* const filePath =
* `${this.protectedPath}/${fileData.originalname}`
* fs.copyFileSync(fileData.path, filePath)
* return {
* url: `${this.serverUrl}/${filePath}`,
* key: filePath
* }
* }
* // ...
* }
* ```
*
* :::tip
*
* This example does not account for duplicate names to maintain simplicity in this guide. So, an uploaded file can replace another existing file that has the same name.
*
* :::
*/
uploadProtected(file: Express.Multer.File): Promise<FileServiceUploadResult>
/**
* remove file from fileservice
* @param fileData Remove file described by record
* */
* This method is used to delete a file from storage.
*
* @param {DeleteFileType} fileData - The details of the file to remove.
* @returns {Promise<void>} Resolves when the file is deleted successfully.
*
* @example
* class LocalFileService extends AbstractFileService {
*
* async delete(
* fileData: DeleteFileType
* ): Promise<void> {
* fs.rmSync(fileData.fileKey)
* }
*
* // ...
* }
*/
delete(fileData: DeleteFileType): Promise<void>
/**
* upload file to fileservice from stream
* @param fileData file metadata relevant for fileservice to create and upload the file
* */
* This method is used to retrieve a write stream to be used to upload a file.
*
* @param {UploadStreamDescriptorType} fileData - The details of the file being uploaded.
* @returns {Promise<FileServiceGetUploadStreamResult>} The result of the file-stream upload.
*
* @example
* // ...
* import { Stream } from "stream"
*
* class LocalFileService extends AbstractFileService {
* // ...
* async getUploadStreamDescriptor({
* name,
* ext,
* isPrivate = true,
* }: UploadStreamDescriptorType
* ): Promise<FileServiceGetUploadStreamResult> {
* const filePath = `${isPrivate ?
* this.publicPath : this.protectedPath
* }/${name}.${ext}`
*
* const pass = new Stream.PassThrough()
* const writeStream = fs.createWriteStream(filePath)
*
* pass.pipe(writeStream)
*
* return {
* writeStream: pass,
* promise: Promise.resolve(),
* url: `${this.serverUrl}/${filePath}`,
* fileKey: filePath,
* }
* }
* // ...
* }
*/
getUploadStreamDescriptor(
fileData: UploadStreamDescriptorType
): Promise<FileServiceGetUploadStreamResult>
/**
* download file from fileservice as stream
* @param fileData file metadata relevant for fileservice to download the file
* @returns readable stream of the file to download
* */
* This method is used to retrieve a read stream for a file, which can then be used to download the file.
*
* @param {GetUploadedFileType} fileData - The details of the file.
* @returns {Promise<NodeJS.ReadableStream>} The [read stream](https://nodejs.org/api/webstreams.html#class-readablestream) to read and download the file.
*
* @example
* class LocalFileService extends AbstractFileService {
*
* async getDownloadStream({
* fileKey,
* isPrivate = true,
* }: GetUploadedFileType
* ): Promise<NodeJS.ReadableStream> {
* const filePath = `${isPrivate ?
* this.publicPath : this.protectedPath
* }/${fileKey}`
* const readStream = fs.createReadStream(filePath)
*
* return readStream
* }
*
* // ...
* }
*/
getDownloadStream(
fileData: GetUploadedFileType
): Promise<NodeJS.ReadableStream>
/**
* Generate a presigned download url to obtain a file
* @param fileData file metadata relevant for fileservice to download the file
* @returns presigned url to download the file
* */
* This method is used to retrieve a download URL of the file. For some file services, such as S3, a presigned URL indicates a temporary URL to get access to a file.
*
* If your file service doesnt perform or offer a similar functionality, you can just return the URL to download the file.
*
* @param {GetUploadedFileType} fileData - The details of the file.
* @returns {Promise<string>} The presigned URL to download the file
*
* @example
* class LocalFileService extends AbstractFileService {
*
* async getPresignedDownloadUrl({
* fileKey,
* isPrivate = true,
* }: GetUploadedFileType
* ): Promise<string> {
* // Local upload doesn't provide
* // support for presigned URLs,
* // so just return the file's URL.
*
* const filePath = `${isPrivate ?
* this.publicPath : this.protectedPath
* }/${fileKey}`
* return `${this.serverUrl}/${filePath}`
* }
*
* // ...
* }
*/
getPresignedDownloadUrl(fileData: GetUploadedFileType): Promise<string>
}
/**
* @parentIgnore activeManager_,atomicPhase_,shouldRetryTransaction_,withTransaction
*/
export abstract class AbstractFileService
extends TransactionBaseService
implements IFileService
{
/**
* @ignore
*/
static _isFileService = true
/**
* @ignore
*/
static isFileService(object): object is AbstractFileService {
return object?.constructor?._isFileService
}
/**
* You can use the `constructor` of your file service to access the different services in Medusa through dependency injection.
*
* You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party providers APIs,
* you can initialize it in the constructor and use it in other methods in the service.
*
* Additionally, if youre creating your file service as an external plugin to be installed on any Medusa backend and you want to access the options added for the plugin,
* you can access them in the constructor.
*
* @param {MedusaContainer} container - An instance of `MedusaContainer` that allows you to access other resources, such as services, in your Medusa backend.
* @param {Record<string, unknown>} config - If this file service is created in a plugin, the plugin's options are passed in this parameter.
*
* @example
* // ...
* import { Logger } from "@medusajs/medusa"
* import * as fs from "fs"
*
* class LocalFileService extends AbstractFileService {
* // can also be replaced by an environment variable
* // or a plugin option
* protected serverUrl = "http://localhost:9000"
* protected publicPath = "uploads"
* protected protectedPath = "protected-uploads"
* protected logger_: Logger
*
* constructor({ logger }: InjectedDependencies) {
* // @ts-ignore
* super(...arguments)
* this.logger_ = logger
*
* // for public uploads
* if (!fs.existsSync(this.publicPath)) {
* fs.mkdirSync(this.publicPath)
* }
*
* // for protected uploads
* if (!fs.existsSync(this.protectedPath)) {
* fs.mkdirSync(this.protectedPath)
* }
* }
* // ...
* }
*/
protected constructor(
protected readonly container: MedusaContainer,
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
) {
super(container, config)
}
abstract upload(
fileData: Express.Multer.File
): Promise<FileServiceUploadResult>
@@ -411,6 +411,7 @@ export abstract class AbstractFulfillmentService
/**
* You can use the `constructor` of your fulfillment provider to access the different services in Medusa through dependency injection.
*
* You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party providers APIs, you can initialize it in the constructor and use it in other methods in the service.
* Additionally, if youre creating your fulfillment provider as an external plugin to be installed on any Medusa backend and you want to access the options added for the plugin, you can access it in the constructor.
*
@@ -1,18 +1,196 @@
import { MedusaContainer } from "@medusajs/types"
import { TransactionBaseService } from "./transaction-base-service"
type ReturnedData = {
/**
* @interface
*
* The details of a sent or resent notification.
*/
export type ReturnedData = {
/**
* The receiver of the Notification. For example, if you sent an email to the customer then `to` is the email address of the customer.
* In other cases, it might be a phone number or a username.
*/
to: string
/**
* The status of the sent notification. There are no restriction on the returned status.
*/
status: string
/**
* The data used to send the Notification. For example, if you sent an order confirmation email to the customer, then the `data` object
* might include the order items or the subject of the email. This `data` is necessary if the notification is resent later as you can use the same data.
*/
data: Record<string, unknown>
}
/**
* ## Overview
*
* :::note[Prerequisites]
*
* Before creating a Notification Provider, [install an event bus module](https://docs.medusajs.com/development/events/modules/redis).
*
* :::
*
* A Notification Provider is a provider that handles sending and resending of notifications.
*
* To create a Notification Provider, create a TypeScript or JavaScript file in `src/services`. The name of the file is the name of the provider
* (for example, `sendgrid.ts`). The file must export a class that extends the `AbstractNotificationService` class imported from `@medusajs/medusa`.
*
* For example, create the file `src/services/email-sender.ts` with the following content:
*
* ```ts title="src/services/email-sender.ts"
* import { AbstractNotificationService } from "@medusajs/medusa"
* import { EntityManager } from "typeorm"
*
* class EmailSenderService extends AbstractNotificationService {
* protected manager_: EntityManager
* protected transactionManager_: EntityManager
*
* sendNotification(
* event: string,
* data: unknown,
* attachmentGenerator: unknown
* ): Promise<{
* to: string;
* status: string;
* data: Record<string, unknown>;
* }> {
* throw new Error("Method not implemented.")
* }
* resendNotification(
* notification: unknown,
* config: unknown,
* attachmentGenerator: unknown
* ): Promise<{
* to: string;
* status: string;
* data: Record<string, unknown>;
* }> {
* throw new Error("Method not implemented.")
* }
*
* }
*
* export default EmailSenderService
* ```
*
* ---
*
* ## Identifier Property
*
* The `NotificationProvider` entity has 2 properties: `identifier` and `is_installed`. The value of the `identifier` property in the notification provider
* class is used when the Notification Provider is created in the database.
*
* The value of this property is also used later when you want to subscribe the Notification Provider to events in a [Loader](https://docs.medusajs.com/development/loaders/overview).
*
* For example:
*
* ```ts
* class EmailSenderService extends AbstractNotificationService {
* static identifier = "email-sender"
* // ...
* }
* ```
*
* ---
*/
export interface INotificationService extends TransactionBaseService {
/**
* When an event is triggered that your Notification Provider is registered as a handler for, the [`NotificationService`](https://docs.medusajs.com/references/services/classes/services.NotificationService)
* in the Medusa backend executes this method of your Notification Provider.
*
* In this method, you can perform the necessary operation to send the Notification. For example, you can send an email to the customer when they place an order.
*
* @param {string} event - The name of the event that was triggered. For example, `order.placed`.
* @param {unknown} data - The data payload of the event that was triggered. For example, if the `order.placed` event is triggered,
* the `eventData` object contains the property `id` which is the ID of the order that was placed. You can refer to the
* [Events reference](https://docs.medusajs.com/development/events/events-list) for information on all events and their payloads.
* @param {unknown} attachmentGenerator - If youve previously register an attachment generator to the `NotificationService` using the
* [`registerAttachmentGenerator`](https://docs.medusajs.com/references/services/classes/services.NotificationService#registerattachmentgenerator) method,
* you have access to it here. You can use the `attachmentGenerator` to generate on-demand invoices or other documents. The default value of this parameter is `null`.
* @returns {Promise<ReturnedData>} The sending details.
*
* @example
* class EmailSenderService extends AbstractNotificationService {
* // ...
* async sendNotification(
* event: string,
* data: any,
* attachmentGenerator: unknown
* ): Promise<{
* to: string;
* status: string;
* data: Record<string, unknown>;
* }> {
* if (event === "order.placed") {
* // retrieve order
* const order = await this.orderService.retrieve(data.id)
* // TODO send email
*
* console.log("Notification sent")
* return {
* to: order.email,
* status: "done",
* data: {
* // any data necessary to send the email
* // for example:
* subject: "You placed a new order!",
* items: order.items,
* },
* }
* }
* }
* // ...
* }
*/
sendNotification(
event: string,
data: unknown,
attachmentGenerator: unknown
): Promise<ReturnedData>
/**
* This method is used to resend notifications, which is typically triggered by the
* [Resend Notification API Route](https://docs.medusajs.com/api/admin#notifications_postnotificationsnotificationresend).
*
* @param {unknown} notification - The original [Notification record](https://docs.medusajs.com/references/entities/classes/Notification) that was created after you sent the
* notification with `sendNotification`. It includes the `to` and `data` attributes which are populated originally using the `to` and `data` properties of
* the object you return in {@link sendNotification}.
* @param {unknown} config - The new configuration used to resend the notification. The [Resend Notification API Route](https://docs.medusajs.com/api/admin#notifications_postnotificationsnotificationresend),
* allows you to pass a new `to` field. If specified, it will be available in this config object.
* @param {unknown} attachmentGenerator - f youve previously register an attachment generator to the `NotificationService` using the
* [`registerAttachmentGenerator`](https://docs.medusajs.com/references/services/classes/services.NotificationService#registerattachmentgenerator) method,
* you have access to it here. You can use the `attachmentGenerator` to generate on-demand invoices or other documents. The default value of this parameter is `null`.
* @returns {Promise<ReturnedData>} The resend details.
*
* @example
* class EmailSenderService extends AbstractNotificationService {
* // ...
* async resendNotification(
* notification: any,
* config: any,
* attachmentGenerator: unknown
* ): Promise<{
* to: string;
* status: string;
* data: Record<string, unknown>;
* }> {
* // check if the receiver should be changed
* const to: string = config.to || notification.to
*
* // TODO resend the notification using the same data
* // that is saved under notification.data
*
* console.log("Notification resent")
* return {
* to,
* status: "done",
* data: notification.data, // make changes to the data
* }
* }
* }
*/
resendNotification(
notification: unknown,
config: unknown,
@@ -20,21 +198,78 @@ export interface INotificationService extends TransactionBaseService {
): Promise<ReturnedData>
}
/**
* @parentIgnore activeManager_,atomicPhase_,shouldRetryTransaction_,withTransaction
*/
export abstract class AbstractNotificationService
extends TransactionBaseService
implements INotificationService
{
/**
* @ignore
*/
static _isNotificationService = true
static identifier: string
/**
* @ignore
*/
static isNotificationService(object): boolean {
return object?.constructor?._isNotificationService
}
/**
* @ignore
*/
getIdentifier(): string {
return (this.constructor as any).identifier
}
/**
* You can use the `constructor` of your notification provider to access the different services in Medusa through dependency injection.
*
* You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party providers APIs,
* you can initialize it in the constructor and use it in other methods in the service.
*
* Additionally, if youre creating your notification provider as an external plugin to be installed on any Medusa backend and you want to access the options
* added for the plugin, you can access it in the constructor.
*
* @param {MedusaContainer} container - An instance of `MedusaContainer` that allows you to access other resources, such as services, in your Medusa backend.
* @param {Record<string, unknown>} config - If this notification provider is created in a plugin, the plugin's options are passed in this parameter.
*
* @example
* // ...
* import { AbstractNotificationService, OrderService } from "@medusajs/medusa"
* import { EntityManager } from "typeorm"
*
* class EmailSenderService extends AbstractNotificationService {
* // ...
* protected orderService: OrderService
*
* constructor(container, options) {
* super(container)
* // you can access options here in case you're
* // using a plugin
*
* this.orderService = container.orderService
*
* // you can also initialize a client that
* // communicates with a third-party service.
* this.client = new Client(options)
* }
*
* // ...
* }
*
* export default EmailSenderService
*/
protected constructor(
protected readonly container: MedusaContainer,
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
) {
super(container, config)
}
abstract sendNotification(
event: string,
data: unknown,
@@ -1,24 +1,153 @@
import { MoneyAmount } from "../models"
import { PriceListType } from "../types/price-list"
import { TaxServiceRate } from "../types/tax-service"
import { ITransactionBaseService } from "@medusajs/types"
import { ITransactionBaseService, MedusaContainer } from "@medusajs/types"
import { TransactionBaseService } from "./transaction-base-service"
/**
* ## Overview
*
* The price selection strategy retrieves the best price for a product variant for a specific context such as selected region, taxes applied,
* the quantity in cart, and more.
*
* Medusa provides a default price selection strategy, but you can override it. A price selecion strategy is a TypeScript or JavaScript file in the `src/strategies` directory of your Medusa backend project. It exports a class that extends the `AbstractPriceSelectionStrategy` class.
*
* For example:
*
* ```ts title="src/strategies/price.ts"
* import {
* AbstractPriceSelectionStrategy,
* PriceSelectionContext,
* PriceSelectionResult,
* } from "@medusajs/medusa"
*
* export default class MyStrategy extends
* AbstractPriceSelectionStrategy {
*
* async calculateVariantPrice(
* data: {
* variantId: string;
* quantity?: number
* }[],
* context: PriceSelectionContext
* ): Promise<Map<string, PriceSelectionResult>> {
* throw new Error("Method not implemented.")
* }
* }
* ```
*
* ---
*/
export interface IPriceSelectionStrategy extends ITransactionBaseService {
/**
* Calculate the original and discount price for a given variant in a set of
* circumstances described in the context.
* @return pricing details in an object containing the calculated lowest price,
* the default price an all valid prices for the given variant
* This method retrieves one or more product variants' prices. It's used when retrieving product variants or their associated line items.
* It's also used when retrieving other entities that product variants and line items belong to, such as products and carts respectively.
*
* @param data - The necessary data to perform the price selection for each variant ID.
* @param context - The context of the price selection.
* @returns {Promise<Map<string, PriceSelectionResult>>} A map, each key is an ID of a variant, and its value is an object holding the price selection result.
*
* @example
* For example, here's a snippet of how the price selection strategy is implemented in the Medusa backend:
*
* ```ts
* import {
* AbstractPriceSelectionStrategy,
* CustomerService,
* PriceSelectionContext,
* PriceSelectionResult,
* } from "@medusajs/medusa"
*
* type InjectedDependencies = {
* customerService: CustomerService
* }
*
* export default class MyStrategy extends
* AbstractPriceSelectionStrategy {
*
* async calculateVariantPrice(
* data: {
* variantId: string
* quantity?: number
* }[],
* context: PriceSelectionContext
* ): Promise<Map<string, PriceSelectionResult>> {
* const dataMap = new Map(data.map((d) => [d.variantId, d]))
*
* const cacheKeysMap = new Map(
* data.map(({ variantId, quantity }) => [
* variantId,
* this.getCacheKey(variantId, { ...context, quantity }),
* ])
* )
*
* const nonCachedData: {
* variantId: string
* quantity?: number
* }[] = []
*
* const variantPricesMap = new Map<string, PriceSelectionResult>()
*
* if (!context.ignore_cache) {
* const cacheHits = await promiseAll(
* [...cacheKeysMap].map(async ([, cacheKey]) => {
* return await this.cacheService_.get<PriceSelectionResult>(cacheKey)
* })
* )
*
* if (!cacheHits.length) {
* nonCachedData.push(...dataMap.values())
* }
*
* for (const [index, cacheHit] of cacheHits.entries()) {
* const variantId = data[index].variantId
* if (cacheHit) {
* variantPricesMap.set(variantId, cacheHit)
* continue
* }
*
* nonCachedData.push(dataMap.get(variantId)!)
* }
* } else {
* nonCachedData.push(...dataMap.values())
* }
*
* let results: Map<string, PriceSelectionResult> = new Map()
*
* if (
* this.featureFlagRouter_.isFeatureEnabled(
* TaxInclusivePricingFeatureFlag.key
* )
* ) {
* results = await this.calculateVariantPrice_new(nonCachedData, context)
* } else {
* results = await this.calculateVariantPrice_old(nonCachedData, context)
* }
*
* await promiseAll(
* [...results].map(async ([variantId, prices]) => {
* variantPricesMap.set(variantId, prices)
* if (!context.ignore_cache) {
* await this.cacheService_.set(cacheKeysMap.get(variantId)!, prices)
* }
* })
* )
*
* return variantPricesMap
* }
*
* // ...
* }
* ```
*/
calculateVariantPrice(
data: {
/**
* The variant id of the variant for which to retrieve prices
* The ID of the variant to retrieve its prices.
*/
variantId: string
/**
* The variant's quantity.
* The variant's quantity in the cart, if available.
*/
quantity?: number
}[],
@@ -29,25 +158,114 @@ export interface IPriceSelectionStrategy extends ITransactionBaseService {
): Promise<Map<string, PriceSelectionResult>>
/**
* Notify price selection strategy that variants prices have been updated.
* @param variantIds The ids of the updated variants
* This method is called when prices of product variants have changed.
* You can use it to invalidate prices stored in the cache.
*
* @param {string[]} variantIds - The IDs of the updated variants.
* @returns {Promise<void>} Resolves after any necessary actions are performed.
*
* @example
* For example, this is how this method is implemented in the Medusa backend's default
* price selection strategy:
*
* ```ts
* import {
* AbstractPriceSelectionStrategy,
* CustomerService,
* } from "@medusajs/medusa"
* import { promiseAll } from "@medusajs/utils"
*
* type InjectedDependencies = {
* customerService: CustomerService
* }
*
* export default class MyStrategy extends
* AbstractPriceSelectionStrategy {
*
* public async onVariantsPricesUpdate(variantIds: string[]): Promise<void> {
* await promiseAll(
* variantIds.map(
* async (id: string) => await this.cacheService_.invalidate(`ps:${id}:*`)
* )
* )
* }
*
* // ...
* }
* ```
*
* :::note
*
* Learn more about the cache service in [this documentation](https://docs.medusajs.com/development/cache/overview).
*
* :::
*/
onVariantsPricesUpdate(variantIds: string[]): Promise<void>
}
/**
* @parentIgnore activeManager_,atomicPhase_,shouldRetryTransaction_,withTransaction
*/
export abstract class AbstractPriceSelectionStrategy
extends TransactionBaseService
implements IPriceSelectionStrategy
{
/**
* @ignore
*/
static _isPriceSelectionStrategy = true
/**
* @ignore
*/
static isPriceSelectionStrategy(object): boolean {
return object?.constructor?._isPriceSelectionStrategy
}
/**
* You can use the `constructor` of your price-selection strategy to access the different services in Medusa through dependency injection.
*
* @param {MedusaContainer} container - An instance of `MedusaContainer` that allows you to access other resources, such as services, in your Medusa backend.
* @param {Record<string, unknown>} config - If this price-selection strategy is created in a plugin, the plugin's options are passed in this parameter.
*
* @example
* // ...
* import {
* AbstractPriceSelectionStrategy,
* CustomerService,
* } from "@medusajs/medusa"
* type InjectedDependencies = {
* customerService: CustomerService
* }
*
* class MyStrategy extends
* AbstractPriceSelectionStrategy {
*
* protected customerService_: CustomerService
*
* constructor(container: InjectedDependencies) {
* super(container)
* this.customerService_ = container.customerService
* }
*
* // ...
* }
*
* export default MyStrategy
*/
protected constructor(
protected readonly container: MedusaContainer,
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
) {
super(container, config)
}
public abstract calculateVariantPrice(
data: {
variantId: string
/**
* @ignore
*/
taxRates: TaxServiceRate[]
quantity?: number
}[],
@@ -59,18 +277,59 @@ export abstract class AbstractPriceSelectionStrategy
}
}
/**
* @interface
*
* The context of the price selection.
*/
export type PriceSelectionContext = {
/**
* The cart's ID. This is used when the prices are being retrieved for the variant of a line item,
* as it is used to determine the current region and currency code of the context.
*/
cart_id?: string
/**
* The ID of the customer viewing the variant.
*/
customer_id?: string
/**
* The region's ID.
*/
region_id?: string
/**
* The quantity of the item in the cart. This is used to filter out price lists that have
* `min_quantity` or `max_quantity` conditions set.
*/
quantity?: number
/**
* The currency code the customer is using.
*/
currency_code?: string
/**
* Whether the price list's prices should be retrieved or not.
*/
include_discount_prices?: boolean
/**
* The tax rates to be applied. This is only used for
* [Tax-Inclusive Pricing](https://docs.medusajs.com/modules/taxes/inclusive-pricing).
*/
tax_rates?: TaxServiceRate[]
/**
* Whether to calculate the prices even if the value of an earlier price calculation
* is available in the cache.
*/
ignore_cache?: boolean
}
/**
* @enum
*
* The type of default price type.
*/
enum DefaultPriceType {
/**
* The `calculatedPrice` is the original price.
*/
DEFAULT = "default",
}
@@ -78,11 +337,38 @@ enum DefaultPriceType {
export type PriceType = DefaultPriceType | PriceListType
export const PriceType = { ...DefaultPriceType, ...PriceListType }
/**
* @interface
*
* The price selection result of a variant.
*/
export type PriceSelectionResult = {
/**
* The original price of the variant which depends on the selected region or currency code in the context object.
* If both region ID and currency code are available in the context object, the region has higher precedence.
*/
originalPrice: number | null
/**
* Whether the original price includes taxes or not. This is only available
* for [Tax-Inclusive Pricing](https://docs.medusajs.com/modules/taxes/inclusive-pricing).
*/
originalPriceIncludesTax?: boolean | null
/**
* The lowest price among the prices of the product variant retrieved using the context object.
*/
calculatedPrice: number | null
/**
* Whether the calculated price includes taxes or not.
* This is only available for [Tax-Inclusive Pricing](https://docs.medusajs.com/modules/taxes/inclusive-pricing).
*/
calculatedPriceIncludesTax?: boolean | null
/**
* The type of price applied in `calculatedPrice`.
*/
calculatedPriceType?: PriceType
prices: MoneyAmount[] // prices is an array of all possible price for the input customer and region prices
/**
* All possible prices of the variant that are retrieved using the `context` object.
* It can include its original price and its price lists if there are any.
*/
prices: MoneyAmount[]
}
@@ -3,15 +3,95 @@ import { TaxCalculationContext } from "./tax-service"
import { LineItemTaxLine } from "../models/line-item-tax-line"
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
import { TransactionBaseService } from "./transaction-base-service"
import { MedusaContainer } from "@medusajs/types"
/**
* ## Overview
*
* A tax calculation strategy is used to calculate taxes when calculating a cart's totals. The Medusa
* backend provides a tax calculation strategy that handles calculating the taxes, taking into account the
* defined tax rates and settings such as whether tax-inclusive pricing is enabled.
*
* You can override the tax calculation strategy to implement different calculation logic or to
* integrate a third-party service that handles the tax calculation. You can override it either
* in a Medusa backend setup or in a plugin.
*
* A tax calculation strategy should be defined in a TypeScript or JavaScript file created under the `src/strategies` directory.
* The class must also implement the `ITaxCalculationStrategy` interface imported from the `@medusajs/medusa` package.
*
* For example, you can create the file `src/strategies/tax-calculation.ts` with the following content:
*
* ```ts title="src/strategies/tax-calculation.ts"
* import {
* ITaxCalculationStrategy,
* LineItem,
* LineItemTaxLine,
* ShippingMethodTaxLine,
* TaxCalculationContext,
* } from "@medusajs/medusa"
*
* class TaxCalculationStrategy
* implements ITaxCalculationStrategy {
*
* async calculate(
* items: LineItem[],
* taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
* calculationContext: TaxCalculationContext
* ): Promise<number> {
* throw new Error("Method not implemented.")
* }
*
* }
*
* export default TaxCalculationStrategy
* ```
*
* ---
*/
export interface ITaxCalculationStrategy {
/**
* Calculates the tax amount for a given set of line items under applicable
* This method calculates the tax amount for a given set of line items under applicable
* tax conditions and calculation contexts.
* @param items - the line items to calculate the tax total for
* @param taxLines - the tax lines that applies to the calculation
* @param calculationContext - other details relevant for the calculation
* @return the tax total
*
* This method is used whenever taxes are calculated. If automatic tax calculation is disabled in a region,
* then it's only triggered when taxes are calculated manually as explained in
* [this guide](https://docs.medusajs.com/modules/taxes/storefront/manual-calculation).
*
* @param {LineItem[]} items - The line items to calculate the tax total for.
* @param {(ShippingMethodTaxLine | LineItemTaxLine)[]} taxLines - The tax lines used for the calculation
* @param {TaxCalculationContext} calculationContext - Other details relevant for the calculation
* @returns {Promise<number>} The calculated tax total
*
* @example
* An example of the general implementation of this method in the Medusa backend's tax calculation strategy:
*
* ```ts
* async calculate(
* items: LineItem[],
* taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
* calculationContext: TaxCalculationContext
* ): Promise<number> {
* const lineItemsTaxLines = taxLines.filter(
* (tl) => "item_id" in tl
* ) as LineItemTaxLine[]
* const shippingMethodsTaxLines = taxLines.filter(
* (tl) => "shipping_method_id" in tl
* ) as ShippingMethodTaxLine[]
*
* const lineItemsTax = this.calculateLineItemsTax(
* items,
* lineItemsTaxLines,
* calculationContext
* )
*
* const shippingMethodsTax = this.calculateShippingMethodsTax(
* calculationContext.shipping_methods,
* shippingMethodsTaxLines
* )
*
* return Math.round(lineItemsTax + shippingMethodsTax)
* }
* ```
*/
calculate(
items: LineItem[],
@@ -20,12 +100,21 @@ export interface ITaxCalculationStrategy {
): Promise<number>
}
/**
* @parentIgnore activeManager_,atomicPhase_,shouldRetryTransaction_,withTransaction
*/
export abstract class AbstractTaxCalculationStrategy
extends TransactionBaseService
implements ITaxCalculationStrategy
{
/**
* @ignore
*/
static _isTaxCalculationStrategy = true
/**
* @ignore
*/
static isTaxCalculationStrategy(object): boolean {
return (
typeof object.calculate === "function" ||
@@ -33,6 +122,46 @@ export abstract class AbstractTaxCalculationStrategy
)
}
/**
* You can use the `constructor` of your tax calculation strategy to access the different services in Medusa through dependency injection.
*
* You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party providers APIs, you can initialize it in the constructor and use it in other methods in the service.
* Additionally, if youre creating your tax calculation strategy as an external plugin to be installed on any Medusa backend and you want to access the options added for the plugin, you can access it in the constructor.
*
* @param {MedusaContainer} container - An instance of `MedusaContainer` that allows you to access other resources, such as services, in your Medusa backend.
* @param {Record<string, unknown>} config - If this tax calculation strategy is created in a plugin, the plugin's options are passed in this parameter.
*
* @example
* import {
* ITaxCalculationStrategy,
* LineItemService,
* } from "@medusajs/medusa"
*
* type InjectedDependencies = {
* lineItemService: LineItemService
* }
*
* class TaxCalculationStrategy
* implements ITaxCalculationStrategy {
*
* protected readonly lineItemService_: LineItemService
*
* constructor({ lineItemService }: InjectedDependencies) {
* this.lineItemService_ = lineItemService
* }
*
* // ...
* }
*
* export default TaxCalculationStrategy
*/
protected constructor(
protected readonly container: MedusaContainer,
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
) {
super(container, config)
}
abstract calculate(
items: LineItem[],
taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
+185 -11
View File
@@ -6,22 +6,35 @@ import { Customer } from "../models/customer"
import { ProviderTaxLine, TaxServiceRate } from "../types/tax-service"
import { LineAllocationsMap } from "../types/totals"
import { TransactionBaseService } from "./transaction-base-service"
import { MedusaContainer } from "@medusajs/types"
/**
* A shipping method and the tax rates that have been configured to apply to the
* A shipping method and the tax rates configured to apply to the
* shipping method.
*/
export type ShippingTaxCalculationLine = {
/**
* The shipping method to calculate taxes for.
*/
shipping_method: ShippingMethod
/**
* The rates applicable on the shipping method.
*/
rates: TaxServiceRate[]
}
/**
* A line item and the tax rates that have been configured to apply to the
* A line item and the tax rates configured to apply to the
* product contained in the line item.
*/
export type ItemTaxCalculationLine = {
/**
* The line item to calculate taxes for.
*/
item: LineItem
/**
* The rates applicable on the item.
*/
rates: TaxServiceRate[]
}
@@ -30,27 +43,135 @@ export type ItemTaxCalculationLine = {
* the items are going.
*/
export type TaxCalculationContext = {
/**
* The shipping address used in the cart.
*/
shipping_address: Address | null
/**
* The customer that the cart belongs to.
*/
customer: Customer
/**
* The cart's region.
*/
region: Region
/**
* Whether the cart is used in a return flow.
*/
is_return: boolean
/**
* The shipping methods used in the cart.
*/
shipping_methods: ShippingMethod[]
/**
* The gift cards and discounts applied on line items.
* Each object key or property is an ID of a line item
*/
allocation_map: LineAllocationsMap
}
/**
* Interface to be implemented by tax provider plugins. The interface defines a
* single method `getTaxLines` that returns numerical rates to apply to line
* items and shipping methods.
* ## Overview
*
* A tax provider is used to retrieve the tax lines in a cart. The Medusa backend provides a default `system` provider. You can create your own tax provider,
* either in a plugin or directly in your Medusa backend, then use it in any region.
*
* A tax provider class is defined in a TypeScript or JavaScript file under the `src/services` directory and the class must extend the
* `AbstractTaxService` class imported from `@medusajs/medusa`. The file's name is the tax provider's class name as a slug and without the word `Service`.
*
* For example, you can create the file `src/services/my-tax.ts` with the following content:
*
* ```ts title="src/services/my-tax.ts"
* import {
* AbstractTaxService,
* ItemTaxCalculationLine,
* ShippingTaxCalculationLine,
* TaxCalculationContext,
* } from "@medusajs/medusa"
* import {
* ProviderTaxLine,
* } from "@medusajs/medusa/dist/types/tax-service"
*
* class MyTaxService extends AbstractTaxService {
* async getTaxLines(
* itemLines: ItemTaxCalculationLine[],
* shippingLines: ShippingTaxCalculationLine[],
* context: TaxCalculationContext):
* Promise<ProviderTaxLine[]> {
* throw new Error("Method not implemented.")
* }
* }
*
* export default MyTaxService
* ```
*
* ---
*
* ## Identifier Property
*
* The `TaxProvider` entity has 2 properties: `identifier` and `is_installed`. The `identifier` property in the tax provider service is used when the tax provider is added to the database.
*
* The value of this property is also used to reference the tax provider throughout Medusa. For example, it is used to [change the tax provider](https://docs.medusajs.com/modules/taxes/admin/manage-tax-settings#change-tax-provider-of-a-region) to a region.
*
* ```ts title="src/services/my-tax.ts"
* class MyTaxService extends AbstractTaxService {
* static identifier = "my-tax"
* // ...
* }
* ```
*
* ---
*/
export interface ITaxService {
/**
* Retrieves the numerical tax lines for a calculation context.
* @param itemLines - the line item calculation lines
* @param itemLines - the shipping calculation lines
* @param context - other details relevant to the tax determination
* @return numerical tax rates that should apply to the provided calculation
* lines
* This method is used when retrieving the tax lines for line items and shipping methods.
* This occurs during checkout or when calculating totals for orders, swaps, or returns.
*
* @param {ItemTaxCalculationLine[]} itemLines - The line item lines to calculate taxes for.
* @param {ShippingTaxCalculationLine[]} shippingLines - The shipping method lines to calculate taxes for.
* @param {TaxCalculationContext} context - Context relevant and useful for the taxes calculation.
* @return {Promise<ProviderTaxLine[]>} The list of calculated line item and shipping method tax lines.
* If an item in the array has the `shipping_method_id` property, then it's a shipping method tax line. Otherwise, if it has
* the `item_id` property, then it's a line item tax line.
*
* @example
* An example of how this method is implemented in the `system` provider implemented in the Medusa backend:
*
* ```ts
* // ...
*
* class SystemTaxService extends AbstractTaxService {
* // ...
*
* async getTaxLines(
* itemLines: ItemTaxCalculationLine[],
* shippingLines: ShippingTaxCalculationLine[],
* context: TaxCalculationContext
* ): Promise<ProviderTaxLine[]> {
* let taxLines: ProviderTaxLine[] = itemLines.flatMap((l) => {
* return l.rates.map((r) => ({
* rate: r.rate || 0,
* name: r.name,
* code: r.code,
* item_id: l.item.id,
* }))
* })
*
* taxLines = taxLines.concat(
* shippingLines.flatMap((l) => {
* return l.rates.map((r) => ({
* rate: r.rate || 0,
* name: r.name,
* code: r.code,
* shipping_method_id: l.shipping_method.id,
* }))
* })
* )
*
* return taxLines
* }
* }
* ```
*/
getTaxLines(
itemLines: ItemTaxCalculationLine[],
@@ -59,17 +180,70 @@ export interface ITaxService {
): Promise<ProviderTaxLine[]>
}
/**
* @parentIgnore activeManager_,atomicPhase_,shouldRetryTransaction_,withTransaction
*/
export abstract class AbstractTaxService
extends TransactionBaseService
implements ITaxService
{
/**
* @ignore
*/
static _isTaxService = true
protected static identifier: string
/**
* @ignore
*/
static isTaxService(object): boolean {
return object?.constructor?._isTaxService
}
/**
* You can use the `constructor` of your tax provider to access the different services in Medusa through dependency injection.
*
* You can also use the constructor to initialize your integration with the third-party provider. For example, if you use a client to connect to the third-party providers APIs, you can initialize it in the constructor and use it in other methods in the service.
* Additionally, if youre creating your tax provider as an external plugin to be installed on any Medusa backend and you want to access the options added for the plugin, you can access it in the constructor.
*
* @param {MedusaContainer} container - An instance of `MedusaContainer` that allows you to access other resources, such as services, in your Medusa backend.
* @param {Record<string, unknown>} config - If this tax provider is created in a plugin, the plugin's options are passed in this parameter.
*
* @example
* // ...
* import { LineItemService } from "@medusajs/medusa"
*
* type InjectedDependencies = {
* lineItemService: LineItemService
* }
*
* class MyTaxService extends AbstractTaxService {
* protected readonly lineItemService_: LineItemService
*
* constructor({ lineItemService }: InjectedDependencies) {
* super(arguments[0])
* this.lineItemService_ = lineItemService
*
* // you can also initialize a client that
* // communicates with a third-party service.
* this.client = new Client(options)
* }
*
* // ...
* }
*
* export default MyTaxService
*/
protected constructor(
protected readonly container: MedusaContainer,
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
) {
super(container, config)
}
/**
* @ignore
*/
public getIdentifier(): string {
if (!(this.constructor as typeof AbstractTaxService).identifier) {
throw new Error(`Missing static property "identifier".`)