<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[perseides - Explore new ways to customize your Medusa.js backend]]></title><description><![CDATA[Explore new ways to customize your Medusa app]]></description><link>https://blog.perseides.org</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1741076694981/e870babf-c29d-455c-bd41-d02188ce8333.png</url><title>perseides - Explore new ways to customize your Medusa.js backend</title><link>https://blog.perseides.org</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Apr 2026 10:01:52 GMT</lastBuildDate><atom:link href="https://blog.perseides.org/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Introducing perseides.org]]></title><description><![CDATA[It's been a while since I've used Hashnode, and for those of you who follow me here, you've probably noticed that no more content has been published here.
Indeed, new articles related to Medusa (and more specifically its new version 2) will be publis...]]></description><link>https://blog.perseides.org/introducing-perseidesorg</link><guid isPermaLink="true">https://blog.perseides.org/introducing-perseidesorg</guid><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Wed, 01 Oct 2025 06:28:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759299867792/790492b2-b0fd-42d4-bc55-46b4e4b98e46.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's been a while since I've used Hashnode, and for those of you who follow me here, you've probably noticed that no more content has been published here.</p>
<p>Indeed, new articles related to Medusa <em>(and more specifically its new version 2)</em> will be published on the <a target="_blank" href="http://perseides.org/blog">perseides.org/blog</a> website. The idea is to eventually centralize plugin-related documentation, as well as technical resources and tutorials.</p>
<p>So feel free to visit there to stay up to date!</p>
]]></content:encoded></item><item><title><![CDATA[Medusa Marketplace #5 | Stripe Connect]]></title><description><![CDATA[Hello everyone!
In this new part, we'll be setting up Stripe and Stripe Connect to collect payments from our customers and trigger payments for our vendors.

⚠
Before starting this part, make sure you have a valid Stripe account and have set up Strip...]]></description><link>https://blog.perseides.org/medusa-marketplace-5-stripe-connect</link><guid isPermaLink="true">https://blog.perseides.org/medusa-marketplace-5-stripe-connect</guid><category><![CDATA[stripe connect]]></category><category><![CDATA[medusa]]></category><category><![CDATA[perseides]]></category><category><![CDATA[medusa.js]]></category><category><![CDATA[marketplace]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[stripe]]></category><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Mon, 27 May 2024 10:51:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716277382984/b1f1cde3-8f03-4755-bc41-1549a0434841.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Hello everyone!</strong></p>
<p>In this new part, we'll be setting up Stripe and Stripe Connect to collect payments from our customers and trigger payments for our vendors.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text"><strong>Before starting this part, </strong><a target="_blank" href="https://stripe.com/"><strong>make sure you have a valid Stripe account and have set up Stripe Connect</strong></a></div>
</div>

<h2 id="heading-updating-the-store-entity">Updating the Store entity</h2>
<p>First, we'll add <strong>two new properties</strong> to the Store entity :</p>
<ul>
<li><p>The first property will be the Stripe account ID linked to a store</p>
</li>
<li><p>The second property will indicate whether the Stripe account is active and ready to receive payments.</p>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/store.ts</span>

<span class="hljs-keyword">import</span> { Column, Entity, OneToMany } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Store <span class="hljs-keyword">as</span> MedusaStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'./product'</span>
<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./user'</span>
<span class="hljs-keyword">import</span> { ShippingOption } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shipping-option'</span>
<span class="hljs-keyword">import</span> { ShippingProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shipping-profile'</span>
<span class="hljs-keyword">import</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'./order'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Store <span class="hljs-keyword">extends</span> MedusaStore {
    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> User, <span class="hljs-function">(<span class="hljs-params">user</span>) =&gt;</span> user.store)
    members?: User[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Product, <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> product.store)
    products?: Product[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> ShippingOption, <span class="hljs-function">(<span class="hljs-params">shippingOption</span>) =&gt;</span> shippingOption.store)
    shippingOptions?: ShippingOption[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> ShippingProfile, <span class="hljs-function">(<span class="hljs-params">shippingProfile</span>) =&gt;</span> shippingProfile.store)
    shippingProfiles?: ShippingProfile[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Order, <span class="hljs-function">(<span class="hljs-params">order</span>) =&gt;</span> order.store)
    orders?: Order[]

    <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> })
    stripe_account_id?: <span class="hljs-built_in">string</span>

    <span class="hljs-meta">@Column</span>({ <span class="hljs-keyword">default</span>: <span class="hljs-literal">false</span> })
    stripe_account_enabled: <span class="hljs-built_in">boolean</span>
}
</code></pre>
<h2 id="heading-creating-the-migration">Creating the Migration</h2>
<p>We can now apply our data to the database by creating our migration :</p>
<pre><code class="lang-apache"><span class="hljs-attribute">npx</span> typeorm migration:create src/migrations/add-stripe-account-to-store
</code></pre>
<p>We can now add our changes in the <code>up</code> and <code>down</code> functions :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/migrations/&lt;TIMESTAMP&gt;-add-stripe-account-to-store.ts</span>

<span class="hljs-comment">// ... export class AddStripe...</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> up(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "store" ADD "stripe_account_id" character varying`</span>)
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "store" ADD "stripe_account_enabled" boolean NOT NULL DEFAULT false`</span>)
    }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> down(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "store" DROP COLUMN "stripe_account_id"`</span>)
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "store" DROP COLUMN "stripe_account_enabled"`</span>)
    }
<span class="hljs-comment">// ... } ...</span>
</code></pre>
<p>Don't forget to apply the migrations once you have updated the file by executing this in your terminal :</p>
<pre><code class="lang-apache"><span class="hljs-attribute">yarn</span> build
<span class="hljs-attribute">npx</span> medusa migrations run
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Whenever you add a new column / property to an entity that have been extended don't forget to update your <code>index.d.ts</code> in case you are going to use that property, or it will throw TypeScript errors.</div>
</div>

<h3 id="heading-configuring-medusa">Configuring Medusa</h3>
<p>In your <code>medusa-config.js</code> at the root of your backend directory, we'll setup the <code>medusa-payment-stripe</code> so that we can use it later :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ... others</span>

<span class="hljs-keyword">const</span> plugins = [
  <span class="hljs-string">`medusa-fulfillment-manual`</span>,
  <span class="hljs-string">`medusa-payment-manual`</span>,
  {
    resolve: <span class="hljs-string">`@medusajs/file-local`</span>,
    options: {
      upload_dir: <span class="hljs-string">"uploads"</span>,
    },
  },
  {
    resolve: <span class="hljs-string">"@medusajs/admin"</span>,
    <span class="hljs-comment">/** @type {import('@medusajs/admin').PluginOptions} */</span>
    options: {
      autoRebuild: <span class="hljs-literal">true</span>,
      develop: {
        open: process.env.OPEN_BROWSER !== <span class="hljs-string">"false"</span>,
      },
    },
  },
  {
    resolve: <span class="hljs-string">`medusa-payment-stripe`</span>,
    options: {
      api_key: process.env.STRIPE_SECRET_KEY,
      capture: <span class="hljs-literal">true</span>, <span class="hljs-comment">// For auto capturing the payments</span>
    },
  },
];

<span class="hljs-comment">// ... others</span>
</code></pre>
<h2 id="heading-adding-the-medusa-payment-stripe-dependency">Adding the <code>medusa-payment-stripe</code> dependency</h2>
<p>Before continuing, make sure you have the <code>medusa-payment-stripe</code> package available in your project, otherwise you can install it this way:</p>
<pre><code class="lang-apache"><span class="hljs-attribute">yarn</span> add medusa-payment-stripe
</code></pre>
<h2 id="heading-creating-the-service">Creating the Service</h2>
<p>With all this groundwork done, we're finally going to create the <code>StripeConnectService</code>. I invite you to create a new file in the <code>/src/services</code> folder with the name <code>stripe-connect.ts</code>.</p>
<p>Once the file has been created, here's what you'll need to paste inside it for now :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { TransactionBaseService, <span class="hljs-keyword">type</span> ConfigModule <span class="hljs-keyword">as</span> MedusaConfigModule } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">'awilix'</span> 
<span class="hljs-keyword">import</span> { MedusaError } <span class="hljs-keyword">from</span> <span class="hljs-string">'medusa-core-utils'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> StripeBase <span class="hljs-keyword">from</span> <span class="hljs-string">'medusa-payment-stripe/dist/core/stripe-base'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Stripe } <span class="hljs-keyword">from</span> <span class="hljs-string">'stripe'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Repository } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/store'</span>

<span class="hljs-keyword">type</span> ConfigModule = MedusaConfigModule &amp; {
    projectConfig: MedusaConfigModule[<span class="hljs-string">'projectConfig'</span>] &amp; {
        server_url: <span class="hljs-built_in">string</span>
    }
}

<span class="hljs-keyword">type</span> InjectedDependencies = {
    stripeProviderService: StripeBase
    storeRepository: Repository&lt;Store&gt;
    configModule: ConfigModule
}

<span class="hljs-keyword">class</span> StripeConnectService <span class="hljs-keyword">extends</span> TransactionBaseService {
    <span class="hljs-keyword">static</span> identifier = <span class="hljs-string">'stripe-connect'</span>
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.SINGLETON

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> stripe_: Stripe
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> serverUrl_: <span class="hljs-built_in">string</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> storeRepository_: Repository&lt;Store&gt;

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">container: InjectedDependencies</span>) {
        <span class="hljs-built_in">super</span>(container)

        <span class="hljs-built_in">this</span>.stripe_ = container.stripeProviderService.getStripe()
        <span class="hljs-built_in">this</span>.serverUrl_= container.configModule.projectConfig.server_url

        <span class="hljs-built_in">this</span>.storeRepository_ = container.storeRepository
    }

    <span class="hljs-keyword">async</span> createTransfer(data: Stripe.TransferCreateParams): <span class="hljs-built_in">Promise</span>&lt;Stripe.Transfer&gt; {
        <span class="hljs-keyword">const</span> transfer = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.stripe_.transfers.create(data)
        <span class="hljs-keyword">return</span> transfer
    }

    <span class="hljs-keyword">async</span> createAccount(storeId: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;Stripe.Response&lt;Stripe.Account&gt;&gt; {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.atomicPhase_(<span class="hljs-keyword">async</span> (m) =&gt; {
            <span class="hljs-keyword">const</span> storeRepo = m.withRepository(<span class="hljs-built_in">this</span>.storeRepository_)

            <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> storeRepo.findOne({ where: { id: storeId } })

            <span class="hljs-keyword">if</span> (!store) {
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(MedusaError.Types.NOT_FOUND, <span class="hljs-string">'Store not found'</span>)
            }

            <span class="hljs-keyword">if</span> (store.stripe_account_id) {
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.stripe_.accounts.retrieve(store.stripe_account_id)
            }

            <span class="hljs-keyword">const</span> accountToken = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.stripe_.tokens.create({
                account: {
                    business_type: <span class="hljs-string">'company'</span>,
                    company: {
                        name: store.name,
                    },
                    tos_shown_and_accepted: <span class="hljs-literal">true</span>,
                },
            })

            <span class="hljs-keyword">const</span> account = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.stripe_.accounts.create({
                <span class="hljs-keyword">type</span>: <span class="hljs-string">'custom'</span>,
                country: <span class="hljs-string">'YOUR_COUNTRY_CODE'</span>, <span class="hljs-comment">// Replace with a supported country code, e.g. 'FR', 'US', 'ES' etc.</span>
                account_token: accountToken.id,
                capabilities: {
                    card_payments: {
                        requested: <span class="hljs-literal">true</span>,
                    },
                    transfers: {
                        requested: <span class="hljs-literal">true</span>,
                    },
                },
            })


            <span class="hljs-keyword">await</span> storeRepo.update(
                storeId,
                { stripe_account_id: account.id }
            )

            <span class="hljs-keyword">return</span> account
        })
    }

    <span class="hljs-keyword">async</span> createOnboardingLink(storeId: <span class="hljs-built_in">string</span>) {
        <span class="hljs-keyword">const</span> url = <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.serverUrl_}</span>/stripe/onboarding`</span>

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.atomicPhase_(<span class="hljs-keyword">async</span> (m) =&gt; {
            <span class="hljs-keyword">const</span> storeRepo = m.withRepository(<span class="hljs-built_in">this</span>.storeRepository_)

            <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> storeRepo.findOne({ where: { id: storeId } })

            <span class="hljs-keyword">if</span> (!store) {
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(MedusaError.Types.NOT_FOUND, <span class="hljs-string">'Store not found'</span>)
            }

            <span class="hljs-keyword">if</span> (!store.stripe_account_id) {
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(MedusaError.Types.NOT_FOUND, <span class="hljs-string">'Stripe account not found'</span>)
            }

            <span class="hljs-keyword">if</span> (store.stripe_account_enabled) {
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(MedusaError.Types.NOT_ALLOWED, <span class="hljs-string">'Stripe account already enabled'</span>)
            }

            <span class="hljs-keyword">const</span> accountLink = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.stripe_.accountLinks.create({
                account: store.stripe_account_id,
                <span class="hljs-keyword">type</span>: <span class="hljs-string">'account_onboarding'</span>,
                refresh_url: <span class="hljs-string">`<span class="hljs-subst">${url}</span>/refresh?storeId=<span class="hljs-subst">${store.id}</span>`</span>,
                return_url: <span class="hljs-string">`<span class="hljs-subst">${url}</span>/return?storeId=<span class="hljs-subst">${store.id}</span>`</span>,
            })


            <span class="hljs-keyword">if</span>(!store.metadata) {
                store.metadata = {
                    stripe_onboarding_url: accountLink.url
                }
            } <span class="hljs-keyword">else</span> {
                store.metadata.stripe_onboarding_url = accountLink.url
            }

            <span class="hljs-keyword">await</span> storeRepo.save(store)
        })
    }


    <span class="hljs-keyword">async</span> retrieveStripeAccount(storeId: <span class="hljs-built_in">string</span>) : <span class="hljs-built_in">Promise</span>&lt;Stripe.Account&gt; {
        <span class="hljs-keyword">const</span> stripeAccountId = (<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.storeRepository_.findOne({ where: { id: storeId } })).stripe_account_id
        <span class="hljs-keyword">const</span> stripeAccount = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.stripe_.accounts.retrieve(stripeAccountId)
        <span class="hljs-keyword">return</span> stripeAccount
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> StripeConnectService
</code></pre>
<p>This service contains all the logic we need to carry out the following process:</p>
<ul>
<li><p><strong>When creating a store</strong>, we'll use the <code>StripeConnectService.createAccount</code> function, which will allow us to create a Stripe account for the store in question.</p>
</li>
<li><p><strong>Once the Stripe account has been created</strong>, you can then create an onboarding link for the Store using the <code>StripeConnectService.createOnboarding</code>. This link will be saved directly in the store metadata in our case, but you can also add a new property to your Store entity that will store the onboarding url if you want.</p>
</li>
<li><p>Finally, once the Stripe account of a store is active, we'll be able to trigger payments using the <code>StripeConnect.createTransfer</code> function <strong>once an order meets our conditions</strong>. In this example, we'll trigger a transfer when an order has been delivered and paid, of course you can adapt to your needs.</p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Don't forget to change the </strong><code>YOUR_COUNTRY_CODE</code><strong> value in the </strong><code>StripeConnectService.createAccount</code><strong> function</strong></div>
</div>

<h3 id="heading-use-the-service">Use the service</h3>
<p>As mentioned above, we're going to use our new service, when a new store is created.</p>
<p>To do this, we'll update our Subscriber responsible for monitoring the <code>StoreService.Events.CREATED</code> event :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Logger, MedusaContainer, SubscriberArgs, SubscriberConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { EntityManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> ShippingProfileService <span class="hljs-keyword">from</span> <span class="hljs-string">'../services/shipping-profile'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> StripeConnectService <span class="hljs-keyword">from</span> <span class="hljs-string">'../services/stripe-connect'</span>
<span class="hljs-keyword">import</span> StoreService <span class="hljs-keyword">from</span> <span class="hljs-string">'../services/store'</span>


<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleStoreCreated</span>(<span class="hljs-params">{
    data,
    eventName,
    container,
    pluginOptions,
}: SubscriberArgs&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;</span>) </span>{
    <span class="hljs-keyword">const</span> logger = container.resolve&lt;Logger&gt;(<span class="hljs-string">"logger"</span>)

    <span class="hljs-keyword">const</span> shippingProfileActivity = logger.activity(<span class="hljs-string">`Creating default shipping profile for store <span class="hljs-subst">${data.id}</span>`</span>)
    <span class="hljs-keyword">await</span> createDefaultShippingProfile(data.id, container).catch(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
        logger.failure(shippingProfileActivity, <span class="hljs-string">`Error creating default shipping profile for store <span class="hljs-subst">${data.id}</span>`</span>)
        <span class="hljs-keyword">throw</span> e
    })
    logger.success(shippingProfileActivity, <span class="hljs-string">`Default shipping profile for store <span class="hljs-subst">${data.id}</span> created`</span>)

    <span class="hljs-keyword">const</span> stripeAccountActivity = logger.activity(<span class="hljs-string">`Creating stripe account for store <span class="hljs-subst">${data.id}</span>`</span>)
    <span class="hljs-keyword">await</span> createStripeAccount(data.id, container).catch(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
        logger.failure(stripeAccountActivity, <span class="hljs-string">`Error creating stripe account for store <span class="hljs-subst">${data.id}</span>`</span>)
        <span class="hljs-keyword">throw</span> e
    })

    logger.success(stripeAccountActivity, <span class="hljs-string">`Stripe account for store <span class="hljs-subst">${data.id}</span> created`</span>)

    <span class="hljs-keyword">const</span> stripeOnboardingActivity = logger.activity(<span class="hljs-string">`Creating stripe onboarding link for store <span class="hljs-subst">${data.id}</span>`</span>)
    <span class="hljs-keyword">await</span> createStripeOnboardingLink(data.id, container).catch(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
        logger.failure(stripeOnboardingActivity, <span class="hljs-string">`Error creating stripe onboarding link for store <span class="hljs-subst">${data.id}</span>`</span>)
        <span class="hljs-keyword">throw</span> e
    })
    logger.success(stripeOnboardingActivity, <span class="hljs-string">`Stripe onboarding link for store <span class="hljs-subst">${data.id}</span> created`</span>)

}


<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createDefaultShippingProfile</span>(<span class="hljs-params">storeId: <span class="hljs-built_in">string</span>, container: MedusaContainer</span>) </span>{
    <span class="hljs-keyword">const</span> manager = container.resolve&lt;EntityManager&gt;(<span class="hljs-string">"manager"</span>)
    <span class="hljs-keyword">const</span> shippingProfileService = container.resolve&lt;ShippingProfileService&gt;(<span class="hljs-string">"shippingProfileService"</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> manager.transaction(<span class="hljs-keyword">async</span> (m) =&gt; {
        <span class="hljs-keyword">await</span> shippingProfileService.withTransaction(m).createDefaultForStore(storeId)
    })
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createStripeAccount</span>(<span class="hljs-params">storeId: <span class="hljs-built_in">string</span>, container: MedusaContainer</span>) </span>{
    <span class="hljs-keyword">const</span> manager = container.resolve&lt;EntityManager&gt;(<span class="hljs-string">"manager"</span>)
    <span class="hljs-keyword">const</span> stripeConnectService = container.resolve&lt;StripeConnectService&gt;(<span class="hljs-string">"stripeConnectService"</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> manager.transaction(<span class="hljs-keyword">async</span> (m) =&gt; {
        <span class="hljs-keyword">await</span> stripeConnectService.withTransaction(m).createAccount(storeId)
    })
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createStripeOnboardingLink</span>(<span class="hljs-params">storeId: <span class="hljs-built_in">string</span>, container: MedusaContainer</span>) </span>{
    <span class="hljs-keyword">const</span> manager = container.resolve&lt;EntityManager&gt;(<span class="hljs-string">"manager"</span>)
    <span class="hljs-keyword">const</span> stripeConnectService = container.resolve&lt;StripeConnectService&gt;(<span class="hljs-string">"stripeConnectService"</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> manager.transaction(<span class="hljs-keyword">async</span> (m) =&gt; {
        <span class="hljs-keyword">await</span> stripeConnectService.withTransaction(m).createOnboardingLink(storeId)
    })
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: SubscriberConfig = {
    event: StoreService.Events.CREATED,
    context: {
        subscriberId: <span class="hljs-string">'store-created-handler'</span>,
    },
}
</code></pre>
<p>Here we've refactored our code a little bit, to have three different functions triggered in order when a store is created, we also have a logger that will allows us to understand easily what's going on.</p>
<p>First it will create the default shipping profile for the new store, then it will create a Stripe account for that store and finally the onboarding link will be created and saved into the Store metadata, so we'll use it inside our Admin UI.</p>
<h3 id="heading-update-again-our-medusa-config">Update (again) our Medusa config</h3>
<p>As you may have noticed, in our service we're extending Medusa's configuration and we've added a new property: <code>server_url</code></p>
<p>In fact, this property doesn't exist by default, so you can add it directly to your configuration in this way to point to the url of your Medusa backend so that Stripe can redirect you back to this URL in the case of a success or error following their onboarding process.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// medusa-config.js</span>

<span class="hljs-comment">// ... others</span>

<span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('@medusajs/medusa').ConfigModule["projectConfig"]}</span> </span>*/</span>
<span class="hljs-keyword">const</span> projectConfig = {
  <span class="hljs-attr">jwtSecret</span>: process.env.JWT_SECRET,
  <span class="hljs-attr">cookieSecret</span>: process.env.COOKIE_SECRET,
  <span class="hljs-attr">store_cors</span>: STORE_CORS,
  <span class="hljs-attr">database_url</span>: DATABASE_URL,
  <span class="hljs-attr">admin_cors</span>: ADMIN_CORS,
  <span class="hljs-attr">server_url</span>: process.env.SERVER_URL || <span class="hljs-string">'http://localhost:9000'</span>
  <span class="hljs-comment">// Uncomment the following lines to enable REDIS</span>
  <span class="hljs-comment">// redis_url: REDIS_URL</span>
};

<span class="hljs-comment">// ...</span>
</code></pre>
<h3 id="heading-testing-our-service-flow">Testing our service flow</h3>
<p>If you try to create a new user, you should see something like this in your terminal after a few seconds :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716411752543/50dd175f-e1eb-4919-bfbc-22170ce8232c.png" alt class="image--center mx-auto" /></p>
<p>Perfect, if you take a look at your <code>store</code> table in your database, you should see a store with <strong>not null</strong> metadata column, which contains the link to start Stripe's onboarding process.</p>
<p>On the other hand, before starting a process, we need to add the two routes that will handle the success/leave and error cases</p>
<h2 id="heading-adding-the-stripe-api-routes">Adding the Stripe API routes</h2>
<h3 id="heading-create-the-success-leave-route">Create the Success / Leave route</h3>
<p>For this section, I invite you to create a new file in the <code>/src/api/stripe/onboarding/return/route.ts</code> folder :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/api/stripe/onboarding/return/route.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Logger, MedusaRequest, MedusaResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { EntityManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../../../models/store'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> StripeConnectService <span class="hljs-keyword">from</span> <span class="hljs-string">'../../../../services/stripe-connect'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">req: MedusaRequest, res: MedusaResponse</span>) </span>{
    <span class="hljs-keyword">const</span> storeId = req.query.storeId

    <span class="hljs-keyword">const</span> logger = req.scope.resolve&lt;Logger&gt;(<span class="hljs-string">'logger'</span>)

    <span class="hljs-keyword">if</span> (!storeId) {
        logger.error(<span class="hljs-string">'Stripe Onboarding Return Route: Missing storeId'</span>)
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'Missing storeId'</span> })
    }

    <span class="hljs-keyword">const</span> stripeConnectService = req.scope.resolve&lt;StripeConnectService&gt;(<span class="hljs-string">'stripeConnectService'</span>)
    <span class="hljs-keyword">const</span> stripeAccount = <span class="hljs-keyword">await</span> stripeConnectService.retrieveStripeAccount(storeId <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>)

    <span class="hljs-keyword">if</span> (!stripeAccount.details_submitted || !stripeAccount.payouts_enabled) {
        <span class="hljs-comment">// Redirect to admin dashboard on onboarding not completed</span>
        res.redirect(<span class="hljs-string">"http://localhost:7001"</span>)
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">const</span> manager: EntityManager = req.scope.resolve&lt;EntityManager&gt;(<span class="hljs-string">'manager'</span>)

    <span class="hljs-keyword">await</span> manager.transaction(<span class="hljs-keyword">async</span> (manager) =&gt; {
        <span class="hljs-keyword">const</span> storeRepo = manager.getRepository(Store)

        <span class="hljs-keyword">let</span> store = <span class="hljs-keyword">await</span> storeRepo.findOne({ where: { id: storeId <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span> } })

        <span class="hljs-keyword">if</span> (!store) {
            logger.error(<span class="hljs-string">'Stripe Onboarding Return Route: Store not found'</span>)
            <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Store not found'</span> })
        }

        <span class="hljs-keyword">if</span> (store.stripe_account_enabled) {
            logger.error(<span class="hljs-string">'Stripe Onboarding Return Route: Stripe account already enabled'</span>)
            <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'Stripe account already enabled'</span> })
        }

        store.stripe_account_enabled = <span class="hljs-literal">true</span>
        store = <span class="hljs-keyword">await</span> storeRepo.save(store)
    })

    <span class="hljs-comment">// Redirect to admin dashboard on success</span>
    res.redirect(<span class="hljs-string">"http://localhost:7001"</span>)
}
</code></pre>
<p>Here, in the case of a process success we retrieve the <code>storeId</code> from the query parameters, and then set the <code>store.stripe_account_enabled</code> value to <code>true</code> for that store. Once the value has been updated, we can redirect the user to the admin UI</p>
<p>But in case the user has just leaved the onboarding process, we do nothing and just redirect.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Of course, you can use a constant here or even the config to make sure it's not hardcoded like this, here, it's just for the example.</strong></div>
</div>

<h3 id="heading-create-the-error-route">Create the Error route</h3>
<p>For this route, in the same <code>/src/api/stripe/onboarding</code> folder, I invite you to create a <code>refresh</code> folder containing this <code>route.ts</code> file :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/api/stripe/onboarding/refresh/route.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Logger, MedusaRequest, MedusaResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { EntityManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../../../models/store'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> StripeConnectService <span class="hljs-keyword">from</span> <span class="hljs-string">'../../../../services/stripe-connect'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">req: MedusaRequest, res: MedusaResponse</span>) </span>{
    <span class="hljs-keyword">const</span> storeId = req.query.storeId
    <span class="hljs-keyword">const</span> logger = req.scope.resolve&lt;Logger&gt;(<span class="hljs-string">'logger'</span>)

    <span class="hljs-keyword">if</span> (!storeId) {
        logger.error(<span class="hljs-string">'Stripe Onboarding Return Route: Missing storeId'</span>)
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'Missing storeId'</span> })
    }

    <span class="hljs-keyword">const</span> manager: EntityManager = req.scope.resolve&lt;EntityManager&gt;(<span class="hljs-string">'manager'</span>)

    <span class="hljs-keyword">let</span> redirectUrl = <span class="hljs-string">''</span>

    <span class="hljs-keyword">await</span> manager.transaction(<span class="hljs-keyword">async</span> (m) =&gt; {
        <span class="hljs-keyword">const</span> stripeConnectService = req.scope.resolve&lt;StripeConnectService&gt;(<span class="hljs-string">'stripeConnectService'</span>)
        <span class="hljs-keyword">const</span> storeRepo = m.getRepository(Store)

        <span class="hljs-keyword">await</span> stripeConnectService.withTransaction(m).createOnboardingLink(storeId <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>)

        <span class="hljs-keyword">const</span> store = <span class="hljs-keyword">await</span> storeRepo.findOne({ where: { id: storeId <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span> } })

        redirectUrl = store.metadata.stripe_onboarding_url <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>
    })

    res.redirect(redirectUrl)
}
</code></pre>
<p>Here, in the case of an error such as an expired URL, we can generate a new onboarding URL and redirect the user directly to it.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can learn more <a target="_blank" href="https://docs.stripe.com/connect/standard-accounts#create-link"><strong>here in the official Stripe docs.</strong></a></div>
</div>

<h2 id="heading-update-the-productservice">Update the <code>ProductService</code></h2>
<p>I suggest that you make sure that the storefront can't access products from stores that haven't activated their Stripe account, to make sure that all stores are "activated" before they can proceed with sales on our platform.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/product.ts</span>

<span class="hljs-comment">// ...</span>

<span class="hljs-keyword">class</span> ProductService <span class="hljs-keyword">extends</span> MedusaProductService {

<span class="hljs-comment">// ...</span>

     <span class="hljs-keyword">async</span> listAndCount(selector: ProductSelector, config?: FindProductConfig): <span class="hljs-built_in">Promise</span>&lt;[Product[], <span class="hljs-built_in">number</span>]&gt; {
        <span class="hljs-keyword">if</span> (!selector.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            selector.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        config.select?.push(<span class="hljs-string">'store_id'</span>)

        config.relations?.push(<span class="hljs-string">'store'</span>)

        <span class="hljs-keyword">const</span> [products, count] = <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.listAndCount(selector, config)

        <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.loggedInUser_) {
            <span class="hljs-comment">// In case we don't have a loggedInUser, we can deduce that it's a storefront API call</span>
            <span class="hljs-comment">// So we filter out the products that have a store with a Stripe account not enabled</span>
            <span class="hljs-keyword">return</span> [products.filter(<span class="hljs-function">(<span class="hljs-params">p</span>) =&gt;</span> p.store.stripe_account_enabled), count]
        }

        <span class="hljs-keyword">return</span> [products, count]
    }
<span class="hljs-comment">// ...</span>
}
</code></pre>
<h2 id="heading-transfers">Transfers</h2>
<p>We will initiate money transfers only when an order has been marked as shipped and its overall status is set to completed.</p>
<p>However, Medusa does not automatically update an order's status to completed when the order is shipped and the payment is captured.</p>
<p>To address this, we need to create a new subscriber that monitors the payment status and fulfillment status. Based on these statuses, we will update the overall order status to <code>completed</code>.</p>
<p>By implementing this new subscriber, we can specifically track when an order is truly completed. Once the order is marked as completed, we can then trigger the money transfer process using our <code>StripeConnectService.createTransfer</code></p>
<h3 id="heading-create-the-child-order-subscriber">Create the Child Order Subscriber</h3>
<p>This subscriber will monitor the events of the child orders (<em>to be clearer, it will monitor the changes made by the stores</em>), if an order has the status of payment captured, and its order has been marked as shipped, then we can update the child order's status to <code>completed</code> and create a transfer for the order's store :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    FulfillmentStatus,
    Logger,
    MedusaContainer,
    OrderStatus,
    PaymentStatus,
    <span class="hljs-keyword">type</span> SubscriberArgs,
    <span class="hljs-keyword">type</span> SubscriberConfig,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../models/order'</span>
<span class="hljs-keyword">import</span> OrderService <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/order'</span>
<span class="hljs-keyword">import</span> StripeConnectService <span class="hljs-keyword">from</span> <span class="hljs-string">'src/services/stripe-connect'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleOrderUpdated</span>(<span class="hljs-params">{
    data,
    eventName,
    container,
    pluginOptions,
}: SubscriberArgs&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;</span>) </span>{
    <span class="hljs-keyword">const</span> logger = container.resolve&lt;Logger&gt;(<span class="hljs-string">'logger'</span>)

    <span class="hljs-keyword">const</span> orderService: OrderService = container.resolve(<span class="hljs-string">'orderService'</span>)

    <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> orderService.retrieve(data.id)

    <span class="hljs-keyword">if</span> (!order.order_parent_id) {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">const</span> updateActivity = logger.activity(<span class="hljs-string">`Updating child order statuses for the parent order <span class="hljs-subst">${order.order_parent_id}</span>...`</span>)
    <span class="hljs-keyword">await</span> updateStatusOfChildren({
        container,
        parentOrderId: order.order_parent_id,
        updateActivity,
        logger
    })
    logger.success(updateActivity, <span class="hljs-string">`Child order statuses updated for the parent order <span class="hljs-subst">${order.order_parent_id}</span>.`</span>)
}

<span class="hljs-comment">/**
 * This function is executed when a child order is updated.
 * It checks if the child order has a payment status of "captured" and a fulfillment status of "shipped".
 * If both conditions are met, it updates the child order's status to "complete", allowing a parent order to be marked as "complete" too.
 * But we also create a Stripe transfer for the store.
 */</span>
<span class="hljs-keyword">type</span> Options = {
    container: MedusaContainer,
    parentOrderId: <span class="hljs-built_in">string</span>,
    updateActivity: <span class="hljs-built_in">void</span>,
    logger: Logger
}
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updateStatusOfChildren</span>(<span class="hljs-params">{
    container,
    parentOrderId,
    logger,
    updateActivity
}: Options</span>) </span>{

    <span class="hljs-keyword">const</span> orderService = container.resolve&lt;OrderService&gt;(<span class="hljs-string">'orderService'</span>)
    <span class="hljs-keyword">const</span> stripeConnectService = container.resolve&lt;StripeConnectService&gt;(<span class="hljs-string">'stripeConnectService'</span>)

    <span class="hljs-keyword">const</span> parentOrder = <span class="hljs-keyword">await</span> orderService.retrieve(parentOrderId, {
        relations: [<span class="hljs-string">'children'</span>],
    })

    <span class="hljs-keyword">if</span> (!parentOrder.children) {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">const</span> ordersToComplete = parentOrder.children
        .filter(<span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span> child.payment_status === PaymentStatus.CAPTURED || child.payment_status === PaymentStatus.PARTIALLY_REFUNDED || child.payment_status === PaymentStatus.REFUNDED)
        .filter(<span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span> child.fulfillment_status === FulfillmentStatus.SHIPPED)
        .filter(
            <span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span>
                child.status !== OrderStatus.CANCELED &amp;&amp;
                child.status !== OrderStatus.ARCHIVED &amp;&amp;
                child.status !== OrderStatus.COMPLETED,
        )

    <span class="hljs-keyword">if</span> (ordersToComplete.length === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> order <span class="hljs-keyword">of</span> ordersToComplete) {
        <span class="hljs-keyword">await</span> orderService.completeOrder(order.id)

        <span class="hljs-keyword">const</span> childOrder = <span class="hljs-keyword">await</span> orderService.retrieveWithTotals(order.id, {
            relations: [<span class="hljs-string">'store'</span>]
        })


        <span class="hljs-keyword">await</span> stripeConnectService.createTransfer({
            amount: childOrder.total - childOrder.refunded_total,
            currency: childOrder.currency_code,
            destination: childOrder.store.stripe_account_id,
            metadata: {
                orderId: childOrder.id,
                orderNumber: childOrder.display_id,
            },
        }).catch(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
            logger.failure(updateActivity, <span class="hljs-string">`An error has occured while creating the Stripe transfer for order <span class="hljs-subst">${order.id}</span>.`</span>)
            <span class="hljs-keyword">throw</span> e
        })
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: SubscriberConfig = {
    event: [
        OrderService.Events.UPDATED,
        OrderService.Events.FULFILLMENT_CREATED,
        OrderService.Events.FULFILLMENT_CANCELED,
        OrderService.Events.GIFT_CARD_CREATED,
        OrderService.Events.ITEMS_RETURNED,
        OrderService.Events.PAYMENT_CAPTURED,
        OrderService.Events.PAYMENT_CAPTURE_FAILED,
        OrderService.Events.REFUND_CREATED,
        OrderService.Events.REFUND_FAILED,
        OrderService.Events.RETURN_ACTION_REQUIRED,
        OrderService.Events.RETURN_REQUESTED,
        OrderService.Events.SHIPMENT_CREATED,
        OrderService.Events.SWAP_CREATED,
    ],
    context: {
        subscriberId: <span class="hljs-string">'child-order-updated-handler'</span>,
    },
}
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Please note that this is purely for educational purposes. In a real marketplace, you wouldn't make transfers directly like this, it implies more thoughts. For example, a transfer could be triggered after a certain time to make sure that no refund could be requested from the customer. <a target="_blank" href="https://docs.medusajs.com/development/scheduled-jobs/create">Take a look at the Scheduled Job with Medusa.</a></div>
</div>

<h2 id="heading-creating-an-admin-widget">Creating an Admin Widget</h2>
<p>Now that the backend is in place, we're going to tackle the Admin UI, so that our vendors can begin the process of onboarding with Stripe to be able to receive transfers later on.</p>
<p>You can create a new widget file at the following path <code>/src/admin/widgets/stripe-onboarding-widget.tsx</code> :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { WidgetConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/admin"</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { SVGProps } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>

<span class="hljs-keyword">import</span> { useAdminStore, useLocalStorage } <span class="hljs-keyword">from</span> <span class="hljs-string">'medusa-react'</span>

<span class="hljs-keyword">const</span> StripeOnboardingWidget = <span class="hljs-function">() =&gt;</span> {

    <span class="hljs-keyword">const</span> { store } = useAdminStore()

    <span class="hljs-keyword">const</span> [shouldHideMessage, setHideMessage] = useLocalStorage(<span class="hljs-string">"dismiss_stripe_message"</span>, <span class="hljs-string">"false"</span>)

    <span class="hljs-keyword">if</span> (!store || !store.metadata || !store.metadata.stripe_onboarding_url) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>

    <span class="hljs-keyword">if</span> (store.stripe_account_enabled &amp;&amp; shouldHideMessage === <span class="hljs-string">"false"</span>){
        <span class="hljs-keyword">return</span> &lt;AccountActivatedDisplay hideMessage={<span class="hljs-function">() =&gt;</span> setHideMessage(<span class="hljs-string">"true"</span>)} /&gt;
    }

    <span class="hljs-keyword">if</span> (!store.stripe_account_enabled) {
        <span class="hljs-keyword">return</span> &lt;ActivateAccountDisplay link={store.metadata.stripe_onboarding_url <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>} /&gt;
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>
}


<span class="hljs-keyword">const</span> ActivateAccountDisplay = <span class="hljs-function">(<span class="hljs-params">{ link }: { link: <span class="hljs-built_in">string</span> }</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">"bg-white/60 backdrop-blur-sm border-b-2 py-6 px-32 min-h-[10rem] fixed top-0 right-0 w-full flex items-center justify-between max-w-[calc(100%-240px)]"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center gap-4"</span>&gt;
                &lt;WarningIcon className=<span class="hljs-string">"text-amber-500"</span> /&gt;
                &lt;div className=<span class="hljs-string">"space-1.5"</span>&gt;
                    &lt;h1 className=<span class="hljs-string">"font-medium text-lg tracking-tight text-grey-80"</span>&gt;Activate your Stripe Account&lt;/h1&gt;
                    &lt;p className=<span class="hljs-string">"text-sm text-grey-50"</span>&gt;
                        Activate your account to start selling your products and make a profit on the perseides platform.
                    &lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;a href={link} className=<span class="hljs-string">"px-4 h-9 bg-grey-90 text-white rounded flex items-center text-sm font-medium hover:bg-grey-70 transition-colors duration-300"</span>&gt;
                Activate Now
            &lt;/a&gt;
        &lt;/div&gt;
    )
}

<span class="hljs-keyword">const</span> AccountActivatedDisplay = ({ hideMessage }: { hideMessage: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span> }) =&gt; {
    <span class="hljs-keyword">return</span> (
        &lt;div className=<span class="hljs-string">"bg-white/60 backdrop-blur-sm border-b-2 py-6 px-32 min-h-[10rem] fixed top-0 right-0 w-full flex items-center justify-between max-w-[calc(100%-240px)]"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center gap-4"</span>&gt;
                &lt;CheckIcon className=<span class="hljs-string">"text-green-500"</span> /&gt;
                &lt;div className=<span class="hljs-string">"space-1.5"</span>&gt;
                    &lt;h1 className=<span class="hljs-string">"font-medium text-lg tracking-tight text-grey-80"</span>&gt;Your account has been activated&lt;/h1&gt;
                    &lt;p className=<span class="hljs-string">"text-sm text-grey-50"</span>&gt;
                        Welcome to perseides, your products are now visible on the storefront and available to buy!
                    &lt;/p&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;button onClick={hideMessage} className=<span class="hljs-string">"px-4 h-9 bg-grey-90 text-white rounded flex items-center text-sm font-medium hover:bg-grey-70 transition-colors duration-300"</span>&gt;
                Dismiss
            &lt;/button&gt;
        &lt;/div&gt;
    )
}

<span class="hljs-keyword">const</span> WarningIcon = <span class="hljs-function">(<span class="hljs-params">props: SVGProps&lt;SVGSVGElement&gt;</span>) =&gt;</span> (&lt;svg
    width=<span class="hljs-string">"24"</span>
    height=<span class="hljs-string">"24"</span>
    viewBox=<span class="hljs-string">"0 0 24 24"</span>
    fill=<span class="hljs-string">"none"</span>
    xmlns=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>
    {...props}
&gt;
    &lt;path
        d=<span class="hljs-string">"M12 6C12.5523 6 13 6.44772 13 7V13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13V7C11 6.44772 11.4477 6 12 6Z"</span>
        fill=<span class="hljs-string">"currentColor"</span>
    /&gt;
    &lt;path
        d=<span class="hljs-string">"M12 16C11.4477 16 11 16.4477 11 17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17C13 16.4477 12.5523 16 12 16Z"</span>
        fill=<span class="hljs-string">"currentColor"</span>
    /&gt;
    &lt;path
        fillRule=<span class="hljs-string">"evenodd"</span>
        clipRule=<span class="hljs-string">"evenodd"</span>
        d=<span class="hljs-string">"M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12Z"</span>
        fill=<span class="hljs-string">"currentColor"</span>
    /&gt;
&lt;/svg&gt;)

<span class="hljs-keyword">const</span> CheckIcon = <span class="hljs-function">(<span class="hljs-params">props: SVGProps&lt;SVGSVGElement&gt;</span>) =&gt;</span> (&lt;svg
    width=<span class="hljs-string">"24"</span>
    height=<span class="hljs-string">"24"</span>
    viewBox=<span class="hljs-string">"0 0 24 24"</span>
    fill=<span class="hljs-string">"none"</span>
    xmlns=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>
    {...props}
&gt;
    &lt;path
        d=<span class="hljs-string">"M10.2426 16.3137L6 12.071L7.41421 10.6568L10.2426 13.4853L15.8995 7.8284L17.3137 9.24262L10.2426 16.3137Z"</span>
        fill=<span class="hljs-string">"currentColor"</span>
    /&gt;
    &lt;path
        fillRule=<span class="hljs-string">"evenodd"</span>
        clipRule=<span class="hljs-string">"evenodd"</span>
        d=<span class="hljs-string">"M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21Z"</span>
        fill=<span class="hljs-string">"currentColor"</span>
    /&gt;
&lt;/svg&gt;)

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: WidgetConfig = {
    zone: [<span class="hljs-string">"order.list.before"</span>, <span class="hljs-string">"product.list.before"</span>],
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> StripeOnboardingWidget
</code></pre>
<p>Here we're injecting this widget in several places, the product and order pages.</p>
<p>The orders page, in particular, is the one that appears following a login, perfect for attracting our vendor's attention!</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Other areas are available for injection, you can find them all <a target="_blank" href="https://docs.medusajs.com/admin/widgets#injection-zones">here</a></div>
</div>

<p>You should see the widget like this when you log in with a Store that do not have activated it's account.</p>
<p><img src="https://cdn.discordapp.com/attachments/1230979481922048061/1244007582687952988/image.png?ex=66558618&amp;is=66543498&amp;hm=efb95243ef5915a951c21cacd8d895d9aa8620d3ec25a31dcb56647faaa0e8ef&amp;=" alt /></p>
<p>And by click on the <strong>Activate Now</strong> button, it should redirect you directly to the Stripe page.</p>
<p><img src="https://cdn.discordapp.com/attachments/1230979481922048061/1244007582981689474/image.png?ex=66558618&amp;is=66543498&amp;hm=5d33a5b31b63571a87c77e473ac0fe01e6afe2a86297441aec4b5044aceea6a0&amp;=" alt /></p>
<p>In order to be displayed in this way at the end of the process :</p>
<p><img src="https://cdn.discordapp.com/attachments/1230979481922048061/1244007583606505634/image.png?ex=66558618&amp;is=66543498&amp;hm=a0cdfe4394d1d1ede17408be3f758b72fbbd9185c2a275599467d5787f1c6eb9&amp;=" alt /></p>
<h2 id="heading-recap">Recap</h2>
<p>Throughout this series, we've explored the process of extending and customizing Medusa.</p>
<p>Leveraging Medusa's flexibility, we've enabled users/vendors (call them how you like haha) to create products, define shipping options, manage orders individually and integrate with Stripe for payouts.</p>
<p>While our course has been a rewarding one, it's important to recognize that we've only scratched the surface of Medusa's capabilities.</p>
<p>Platforms such as Amazon and Etsy offer a wide range of features and functionality to meet a variety of <em>"business requirements",</em> however, the knowledge and skills you've gained in this series have provided you with the foundation you need to further develop Medusa's core functionality and apply advanced concepts to your project!</p>
<h2 id="heading-support-my-work">Support my work</h2>
<p>If you've enjoyed this series, or if you'd like to see other parts, feel free to <a target="_blank" href="https://buymeacoffee.com/adevinwild">buy me a coffee by clicking here</a>, or <a target="_blank" href="https://blog.perseides.org/sponsor">become a sponsor of the perseides blog by clicking here</a></p>
<h2 id="heading-github-branch"><strong>GitHub Branch</strong></h2>
<p>You can access the complete part's code <a target="_blank" href="https://git.new/perseides-part-5"><strong>here</strong></a><strong>.</strong></p>
<p>You can also have access to the storefront code <a target="_blank" href="https://github.com/perseidesjs/storefront">here</a>.</p>
<h2 id="heading-contact"><strong>Contact</strong></h2>
<p>You can contact me on Discord and X with the same username : <a target="_blank" href="https://twitter.com/adevinwild"><strong>@adevinwild</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[Medusa Marketplace #4 | Payments Splitting]]></title><description><![CDATA[Hello everyone!
In this new section, we'll take a look at payments in preparation for what's to come.
What's the goal here?
In the same spirit as Orders, we're going to divide a single Payment into several for each store, or to be more transparent, w...]]></description><link>https://blog.perseides.org/medusa-marketplace-4-payments-splitting</link><guid isPermaLink="true">https://blog.perseides.org/medusa-marketplace-4-payments-splitting</guid><category><![CDATA[medusa]]></category><category><![CDATA[medusa.js]]></category><category><![CDATA[Series]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[perseides]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Express]]></category><category><![CDATA[typeorm]]></category><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Wed, 15 May 2024 14:30:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715715319828/f90985f5-d059-49bf-a342-e15ed60d7d01.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Hello everyone!</strong></p>
<p>In this new section, we'll take a look at payments in preparation for what's to come.</p>
<h2 id="heading-whats-the-goal-here">What's the goal here?</h2>
<p>In the same spirit as Orders, we're going to divide a single Payment into several for each store, or to be more transparent, <strong>we're going to create dummy payments for each store.</strong></p>
<p>Let me explain.</p>
<p>The idea behind <em>perseides</em> is to create a marketplace with as little friction as possible. In fact, by creating dummy payments for each child order, we'll be able to calculate the total amount available for each seller on a specific order and we will not have to update a lot of logic. An example is the refund of an order. With this system, the refund will recalculate the child payment (which will be shown to our seller), but also act on the parent payment, which will execute actions on the <a target="_blank" href="https://docs.medusajs.com/modules/carts-and-checkout/payment#payment-processor">PaymentProcessor</a>.</p>
<h2 id="heading-extend-the-entity">Extend the entity</h2>
<p>It's always the same recipe when it comes to extending core features: first, you extend the entity :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/payment.ts</span>
<span class="hljs-keyword">import</span> { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Payment <span class="hljs-keyword">as</span> MedusaPayment } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Payment <span class="hljs-keyword">extends</span> MedusaPayment {
    <span class="hljs-meta">@Index</span>(<span class="hljs-string">'PaymentParentId'</span>)
    <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> })
    payment_parent_id?: <span class="hljs-built_in">string</span>

    <span class="hljs-meta">@ManyToOne</span>(<span class="hljs-function">() =&gt;</span> Payment, <span class="hljs-function">(<span class="hljs-params">payment</span>) =&gt;</span> payment.children)
    <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">'payment_parent_id'</span>, referencedColumnName: <span class="hljs-string">'id'</span> })
    parent?: Payment

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Payment, <span class="hljs-function">(<span class="hljs-params">payment</span>) =&gt;</span> payment.parent)
    <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">'id'</span>, referencedColumnName: <span class="hljs-string">'payment_parent_id'</span> })
    children?: Payment[]
}
</code></pre>
<h2 id="heading-create-the-migration">Create the migration</h2>
<p>Once the entity has been extended, it's time to migrate and apply our changes :</p>
<pre><code class="lang-apache"><span class="hljs-attribute">npx</span> typeorm migration:create src/migrations/add-payment-children
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/migrations/&lt;TIMESTAMP&gt;-add-payment-children.ts</span>

<span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> up(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "payment" ADD "payment_parent_id" character varying`</span>)
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`CREATE INDEX "PaymentParentId" ON "payment" ("payment_parent_id")`</span>)
    }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> down(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`DROP INDEX "public"."PaymentParentId"`</span>)
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "payment" DROP COLUMN "payment_parent_id"`</span>)
    }
<span class="hljs-comment">// ...</span>
</code></pre>
<p>Then we build before executing the migrations :</p>
<pre><code class="lang-apache"><span class="hljs-attribute">yarn</span> build
<span class="hljs-attribute">npx</span> medusa migrations run
</code></pre>
<p>When you refresh your database, the new column <code>payment_parent_id</code> should appear:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715698429711/a6224568-f588-4f21-82b9-7185c15ac576.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-override-the-orderservicecreaterefund-function">Override the <code>OrderService.createRefund</code> function</h2>
<p>Another important aspect concerning refunds with these child payments is that when a refund is made on a child order, the payment used to refund must be the one the parent order (<em>the real payment</em>) :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/order.ts</span>

<span class="hljs-keyword">class</span> OrderService <span class="hljs-keyword">extends</span> MedusaOrderService {
<span class="hljs-comment">// ... rest previously implemented</span>
    <span class="hljs-keyword">async</span> createRefund(orderId: <span class="hljs-built_in">string</span>, refundAmount: <span class="hljs-built_in">number</span>, reason: <span class="hljs-built_in">string</span>, note?: <span class="hljs-built_in">string</span>, config?: { no_notification?: <span class="hljs-built_in">boolean</span> }): <span class="hljs-built_in">Promise</span>&lt;Order&gt; {
        <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.retrieveWithTotals(orderId, { relations: [<span class="hljs-string">'payments'</span>] })

        <span class="hljs-keyword">if</span> (!order.order_parent_id) {
            <span class="hljs-comment">// This means we are refunding from the parent order...</span>
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.createRefund(orderId, refundAmount, reason, note, config)
        }

        <span class="hljs-comment">// Means the refund have occured on a child order,</span>
        <span class="hljs-comment">// so we need to refund from the parent order and compute the new child order amount</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.atomicPhase_(<span class="hljs-keyword">async</span> (m) =&gt; {
            <span class="hljs-keyword">const</span> orderRepo = m.withRepository(<span class="hljs-built_in">this</span>.orderRepository_)
            <span class="hljs-keyword">const</span> refundRepo = m.getRepository(Refund)

            <span class="hljs-comment">// If the refund amount is greater than the order amount, we can't refund</span>
            <span class="hljs-keyword">if</span> (refundAmount &gt; order.refundable_amount) {
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(MedusaError.Types.INVALID_ARGUMENT, <span class="hljs-string">'Refund amount is greater than the order amount'</span>)
            }

            <span class="hljs-comment">// We refund from the parent order</span>
            <span class="hljs-keyword">let</span> parentOrder = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.retrieve(order.order_parent_id)
            parentOrder = <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.createRefund(order.order_parent_id, refundAmount, reason, <span class="hljs-string">`<span class="hljs-subst">${note}</span>\n\nRefund from child order : <span class="hljs-subst">${order.id}</span>`</span>, config)


            <span class="hljs-comment">// We create a refund for the child order, for future computation (refundable_amount)</span>
            <span class="hljs-keyword">const</span> refund = refundRepo.create({
                order_id: order.id,
                amount: refundAmount,
                reason,
                note: <span class="hljs-string">`<span class="hljs-subst">${note}</span>\n\nRefund from child order : <span class="hljs-subst">${order.id}</span>`</span>,
                payment_id: order.payments?.at(<span class="hljs-number">0</span>)?.id
            })
            <span class="hljs-keyword">await</span> refundRepo.save(refund)

            <span class="hljs-comment">// We check if the child order payment is fully refunded</span>
            <span class="hljs-comment">// If it is, we can set the payment status to REFUNDED</span>
            <span class="hljs-comment">// Otherwise, we set the payment status to PARTIALLY_REFUNDED</span>
            <span class="hljs-keyword">const</span> childOrderPayment = order.payments?.at(<span class="hljs-number">0</span>)

            <span class="hljs-keyword">const</span> amountRefunded = childOrderPayment.amount_refunded + refundAmount

            <span class="hljs-keyword">const</span> isFullyRefunded = amountRefunded === childOrderPayment.amount

            <span class="hljs-keyword">await</span> orderRepo.update(order.id, {
                payment_status: isFullyRefunded ? PaymentStatus.REFUNDED : PaymentStatus.PARTIALLY_REFUNDED
            })

            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.retrieve(order.id)
        })
    }

}
</code></pre>
<p>In the code snippet above, we make sure to check whether the order is a parent order or a child order, in the case of a parent order, no worries, the original behavior remains the original, on the other hand, in the case where a vendor requests a refund from a child order, the refund must take place on the parent order</p>
<p>We also create <em>"fake"</em> refunds on that <em>"fake"</em> payment, allowing us for a easier computation of the refundable amount later on.</p>
<h2 id="heading-override-the-orderservicecapturepayment-function">Override the <code>OrderService.capturePayment</code> function</h2>
<p>In this case, we'll make sure that when the payment for a parent order is captured, the payments for the child orders are also captured.</p>
<p>When we will configure the Stripe plugin, the parent payments will be captured automatically using the <code>medusa-payment-stripe</code> plugin options.</p>
<p>But we also needs to update child order payments :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/order.ts</span>

<span class="hljs-keyword">class</span> OrderService <span class="hljs-keyword">extends</span> MedusaOrderService {
<span class="hljs-comment">// ... rest previously implemented</span>

   <span class="hljs-comment">/**
     * When a parent payment is captured, we capture the parent order payment but also every child order payment
     */</span>
    <span class="hljs-keyword">async</span> capturePayment(orderId: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;Order&gt; {
        <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.retrieveWithTotals(orderId, { relations: [<span class="hljs-string">'payments'</span>, <span class="hljs-string">'children'</span>] })

        <span class="hljs-keyword">if</span> (order.order_parent_id) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(
                MedusaError.Types.NOT_ALLOWED,
                <span class="hljs-string">`Order with id <span class="hljs-subst">${orderId}</span> is a child order and cannot be captured.`</span>
            )
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.atomicPhase_(<span class="hljs-keyword">async</span> (m) =&gt; {
            <span class="hljs-keyword">const</span> orderRepo = m.withRepository(<span class="hljs-built_in">this</span>.orderRepository_)
            <span class="hljs-keyword">const</span> paymentRepo = m.withRepository(PaymentRepository)

            <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> child <span class="hljs-keyword">of</span> order.children) {
                <span class="hljs-keyword">const</span> childOrder = <span class="hljs-keyword">await</span> orderRepo.findOne({
                    where: {
                        id: child.id
                    },
                    relations: [<span class="hljs-string">'payments'</span>]
                })

                <span class="hljs-keyword">if</span> (!childOrder.payments.at(<span class="hljs-number">0</span>).captured_at) {
                    <span class="hljs-keyword">await</span> paymentRepo.update(childOrder.payments.at(<span class="hljs-number">0</span>).id, {
                        captured_at: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
                    })
                    <span class="hljs-keyword">await</span> orderRepo.update(child.id, {
                        payment_status: PaymentStatus.CAPTURED
                    })
                }
            }

            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.capturePayment(order.id)
        })
    }

}
</code></pre>
<h2 id="heading-update-the-orderplaced-subscriber">Update the OrderPlaced subscriber</h2>
<p>Once our Payment entity has been modified, we can update our <a target="_blank" href="https://blog.perseides.org/medusa-marketplace-3-orders-splitting#heading-create-the-order-placed-subscriber">previously created subscriber</a> to create a Payment for each child Order created within the subscriber.</p>
<p>Update your code with the following :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    LineItem,
    Logger,
    OrderService,
    PaymentStatus,
    ShippingMethod,
    <span class="hljs-keyword">type</span> SubscriberArgs,
    <span class="hljs-keyword">type</span> SubscriberConfig
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> ShippingOptionService <span class="hljs-keyword">from</span> <span class="hljs-string">'src/services/shipping-option'</span>
<span class="hljs-keyword">import</span> { EntityManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../models/order'</span>
<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../models/product'</span>
<span class="hljs-keyword">import</span> { Payment } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../models/payment'</span>

<span class="hljs-keyword">import</span> ProductService <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/product'</span>


<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleOrderPlaced</span>(<span class="hljs-params">{
    data,
    eventName,
    container,
    pluginOptions,
}: SubscriberArgs&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;</span>) </span>{
    <span class="hljs-keyword">const</span> manager = container.resolve&lt;EntityManager&gt;(<span class="hljs-string">'manager'</span>)
    <span class="hljs-keyword">const</span> logger = container.resolve&lt;Logger&gt;(<span class="hljs-string">'logger'</span>)

    <span class="hljs-keyword">await</span> manager.transaction(<span class="hljs-keyword">async</span> (m) =&gt; {
        <span class="hljs-keyword">const</span> orderService: OrderService = container.resolve&lt;OrderService&gt;(<span class="hljs-string">'orderService'</span>)
        <span class="hljs-keyword">const</span> productService: ProductService = container.resolve&lt;ProductService&gt;(<span class="hljs-string">'productService'</span>)
        <span class="hljs-keyword">const</span> shippingOptionService: ShippingOptionService =
            container.resolve&lt;ShippingOptionService&gt;(<span class="hljs-string">'shippingOptionService'</span>)


        <span class="hljs-keyword">const</span> orderRepo = m.getRepository(Order)
        <span class="hljs-keyword">const</span> lineItemRepo = m.getRepository(LineItem)
        <span class="hljs-keyword">const</span> shippingMethodRepo = m.getRepository(ShippingMethod)
        <span class="hljs-keyword">const</span> paymentRepo = m.getRepository(Payment)

        <span class="hljs-keyword">const</span> orderActivity = logger.activity(<span class="hljs-string">`Splitting order <span class="hljs-subst">${data.id}</span> into child orders...`</span>)

        <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> orderService.retrieveWithTotals(data.id, {
            relations: [<span class="hljs-string">'items'</span>, <span class="hljs-string">'items.variant'</span>, <span class="hljs-string">'items.variant.prices'</span>, <span class="hljs-string">'cart'</span>, <span class="hljs-string">'payments'</span>, <span class="hljs-string">'shipping_methods'</span>],
        })

        <span class="hljs-keyword">if</span> (!order) {
            logger.failure(orderActivity, <span class="hljs-string">`OrderPlacedSubscriber | Order not found for order id <span class="hljs-subst">${data.id}</span>.`</span>)
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-comment">// #1 : Group Items By Store Id</span>

        <span class="hljs-keyword">const</span> storesWithItems = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">string</span>, Order[<span class="hljs-string">'items'</span>]&gt;()

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> order.items) {
            <span class="hljs-keyword">const</span> product: Product = <span class="hljs-keyword">await</span> productService.retrieve(item.variant.product_id)
            <span class="hljs-keyword">const</span> storeId = product.store_id

            <span class="hljs-keyword">if</span> (!storeId) {
                logger.failure(orderActivity, <span class="hljs-string">`OrderPlacedSubscriber | product.store_id not found for product <span class="hljs-subst">${product.id}</span>.`</span>)
                <span class="hljs-keyword">continue</span>
            }

            <span class="hljs-keyword">if</span> (!storesWithItems.has(storeId)) {
                storesWithItems.set(storeId, [])
            }

            storesWithItems.get(storeId).push(item)
        }

        <span class="hljs-comment">// #2 : For each store, create a new order with the relevant items and shipping methods</span>
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> [storeId, items] <span class="hljs-keyword">of</span> storesWithItems.entries()) {

            <span class="hljs-comment">// #2.1 : Create a new order</span>
            <span class="hljs-keyword">const</span> childOrder = orderRepo.create({
                ...order,
                order_parent_id: order.id,
                store_id: storeId,
                cart_id: <span class="hljs-literal">null</span>,
                cart: <span class="hljs-literal">null</span>,
                id: <span class="hljs-literal">null</span>,
                shipping_methods: [],
            })

            <span class="hljs-keyword">const</span> savedChildOrder = <span class="hljs-keyword">await</span> orderRepo.save(childOrder)

            <span class="hljs-comment">// #2.2 : Create a new line item for each item in the order</span>
            <span class="hljs-keyword">let</span> totalItemsAmount: <span class="hljs-built_in">number</span> = <span class="hljs-number">0</span>
            <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> items) {
                <span class="hljs-keyword">const</span> lineItem = lineItemRepo.create({
                    ...item,
                    order_id: savedChildOrder.id,
                    cart_id: <span class="hljs-literal">null</span>,
                    id: <span class="hljs-literal">null</span>,
                })

                <span class="hljs-keyword">await</span> lineItemRepo.save(lineItem)


                <span class="hljs-comment">// We compute the total order amount for the child order</span>
                totalItemsAmount += item.total
            }

            <span class="hljs-comment">// #2.3 : Create a new shipping method for each child order with a matching shipping option that is in the same store</span>
            <span class="hljs-keyword">let</span> totalShippingAmount: <span class="hljs-built_in">number</span> = <span class="hljs-number">0</span>
            <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> shippingMethod <span class="hljs-keyword">of</span> order.shipping_methods) {
                <span class="hljs-keyword">const</span> shippingOption = <span class="hljs-keyword">await</span> shippingOptionService.retrieve(shippingMethod.shipping_option_id)

                <span class="hljs-keyword">if</span> (shippingOption.store_id !== storeId) {
                    <span class="hljs-keyword">continue</span>
                }

                <span class="hljs-keyword">const</span> newShippingMethod = shippingMethodRepo.create({
                    ...shippingMethod,
                    id: <span class="hljs-literal">null</span>,
                    cart_id: <span class="hljs-literal">null</span>,
                    cart: <span class="hljs-literal">null</span>,
                    order_id: savedChildOrder.id,
                })

                <span class="hljs-keyword">await</span> shippingMethodRepo.save(newShippingMethod)

                totalShippingAmount += shippingMethod.total
            }

            <span class="hljs-keyword">const</span> childPayment = paymentRepo.create({
                ...order.payments[<span class="hljs-number">0</span>], <span class="hljs-comment">// In our case, we only have one payment for the order</span>
                payment_parent_id: order.payments[<span class="hljs-number">0</span>].id,
                order_id: savedChildOrder.id,
                amount: totalItemsAmount + totalShippingAmount, <span class="hljs-comment">// This is the total amount of the child order</span>
                cart_id: <span class="hljs-literal">null</span>,
                cart: <span class="hljs-literal">null</span>,
                id: <span class="hljs-literal">null</span>,
            })

            <span class="hljs-keyword">await</span> paymentRepo.save(childPayment)

        }

        <span class="hljs-comment">// #3 : Capture the payment for the parent order (it will also capture the child orders)</span>
        <span class="hljs-keyword">await</span> orderService.withTransaction(m).capturePayment(order.id)

        logger.success(orderActivity, <span class="hljs-string">`Order <span class="hljs-subst">${data.id}</span> has been split into <span class="hljs-subst">${storesWithItems.size}</span> child orders.`</span>)

    })

}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: SubscriberConfig = {
    event: OrderService.Events.PLACED,
    context: {
        subscriberId: <span class="hljs-string">'order-placed-handler'</span>,
    },
}
</code></pre>
<p>From now on, for each order placed, a child payment will be created and linked to a child order.</p>
<p>In the screenshot below, here's an example of an order that has been split into several orders.</p>
<p>You can see that the total amount displayed is explicitly that of the products and shipping costs linked to that specific Store only :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715711427060/a648675b-2f35-4914-bd3e-fcd6881e78ac.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-typescript">   <span class="hljs-comment">/**
     * When a parent payment is captured, we capture the parent order payment but also every child order payment
     */</span>
    <span class="hljs-keyword">async</span> capturePayment(orderId: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;Order&gt; {
        <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.retrieveWithTotals(orderId, { relations: [<span class="hljs-string">'payments'</span>, <span class="hljs-string">'children'</span>] })

        <span class="hljs-keyword">if</span> (order.order_parent_id) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(
                MedusaError.Types.NOT_ALLOWED,
                <span class="hljs-string">`Order with id <span class="hljs-subst">${orderId}</span> is a child order and cannot be captured.`</span>
            )
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.atomicPhase_(<span class="hljs-keyword">async</span> (m) =&gt; {
            <span class="hljs-keyword">const</span> orderRepo = m.withRepository(<span class="hljs-built_in">this</span>.orderRepository_)
            <span class="hljs-keyword">const</span> paymentRepo = m.withRepository(PaymentRepository)

            <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> child <span class="hljs-keyword">of</span> order.children) {
                <span class="hljs-keyword">const</span> childOrder = <span class="hljs-keyword">await</span> orderRepo.findOne({
                    where: {
                        id: child.id
                    },
                    relations: [<span class="hljs-string">'payments'</span>]
                })

                <span class="hljs-keyword">if</span> (!childOrder.payments.at(<span class="hljs-number">0</span>).captured_at) {
                    <span class="hljs-keyword">await</span> paymentRepo.update(childOrder.payments.at(<span class="hljs-number">0</span>).id, {
                        captured_at: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
                    })
                    <span class="hljs-keyword">await</span> orderRepo.update(child.id, {
                        payment_status: PaymentStatus.CAPTURED
                    })
                }
            }

            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.capturePayment(order.id)
        })
    }
</code></pre>
<h2 id="heading-whats-next">What's Next ?</h2>
<p>In the next section, we'll be integrating Stripe and Stripe Connect, so don't forget to create your Stripe and Stripe Connect account.</p>
<h2 id="heading-github-branch">GitHub Branch</h2>
<p>You can access the complete part's code <a target="_blank" href="https://git.new/perseides-part-4"><strong>here</strong></a><strong>.</strong></p>
<h2 id="heading-contact"><strong>Contact</strong></h2>
<p>You can contact me on Discord and X with the same username : <a target="_blank" href="https://twitter.com/adevinwild"><strong>@adevinwild</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[Medusa Marketplace #3 | Orders Splitting]]></title><description><![CDATA[Hello everyone!
Many of you have been waiting for this part, so here we're going to cover how to manage orders for each seller. Indeed, the most logical and widely used way is the one outlined by Shahed Nasserin her tutorial series that inspired pers...]]></description><link>https://blog.perseides.org/medusa-marketplace-3-orders-splitting</link><guid isPermaLink="true">https://blog.perseides.org/medusa-marketplace-3-orders-splitting</guid><category><![CDATA[medusa]]></category><category><![CDATA[medusa.js]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Series]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[marketplace]]></category><category><![CDATA[perseides]]></category><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Fri, 10 May 2024 18:30:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715345348379/b6771659-073b-4c5e-857a-9d9c1debff78.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Hello everyone!</strong></p>
<p>Many of you have been waiting for this part, so here we're going to cover how to manage orders for each seller. Indeed, the most logical and widely used way is the one outlined by <a class="user-mention" href="https://hashnode.com/@shahednasser">Shahed Nasser</a><a target="_blank" href="https://medium.com/medusa-commerce/how-to-create-a-marketplace-with-medusa-part-2-make-orders-specific-to-vendors-ee87a82eb60f"><strong>in her tutorial series</strong></a> that inspired <em>perseides</em>.</p>
<h2 id="heading-whats-the-goal-here">What's the goal here ?</h2>
<p>To ensure that our vendors can process their orders independently, we're going to extend the Order entity to add some new features, for example, we will split an Order into multiple child orders, allowing a vendor to only access its order and the items they need to fulfill.</p>
<h2 id="heading-extend-the-order">Extend the Order</h2>
<h3 id="heading-extend-the-order-entity">Extend the Order entity</h3>
<p>First, we'll extend the entity we're getting used to :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Order <span class="hljs-keyword">as</span> MedusaOrder } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Order <span class="hljs-keyword">extends</span> MedusaOrder {
    <span class="hljs-meta">@Index</span>(<span class="hljs-string">'OrderStoreId'</span>)
    <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> })
    store_id?: <span class="hljs-built_in">string</span>

    <span class="hljs-meta">@ManyToOne</span>(<span class="hljs-function">() =&gt;</span> Store, <span class="hljs-function">(<span class="hljs-params">store</span>) =&gt;</span> store.orders)
    <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">'store_id'</span>, referencedColumnName: <span class="hljs-string">'id'</span> })
    store?: Store

    <span class="hljs-meta">@Index</span>(<span class="hljs-string">'OrderParentId'</span>)
    <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> })
    order_parent_id?: <span class="hljs-built_in">string</span>

    <span class="hljs-meta">@ManyToOne</span>(<span class="hljs-function">() =&gt;</span> Order, <span class="hljs-function">(<span class="hljs-params">order</span>) =&gt;</span> order.children)
    <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">'order_parent_id'</span>, referencedColumnName: <span class="hljs-string">'id'</span> })
    parent?: Order

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Order, <span class="hljs-function">(<span class="hljs-params">order</span>) =&gt;</span> order.parent)
    <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">'id'</span>, referencedColumnName: <span class="hljs-string">'order_parent_id'</span> })
    children?: Order[]
}
</code></pre>
<p>Here we have added :</p>
<ul>
<li><p>a <code>store_id</code> as on our previous entities</p>
</li>
<li><p>a new <code>order_parent_id</code> column, which is nullable, allowing us to know whether an order has a parent or not</p>
</li>
<li><p>relations that allow us to find out the parent or children of an order</p>
</li>
</ul>
<h3 id="heading-updating-the-store-entity">Updating the Store entity</h3>
<p>As usual, we've added a relationship that points to our Store, so we'll need to update it to add the orders property to the Store entity :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/store.ts</span>
<span class="hljs-keyword">import</span> { Entity, OneToMany } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Store <span class="hljs-keyword">as</span> MedusaStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'./product'</span>
<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./user'</span>
<span class="hljs-keyword">import</span> { ShippingOption } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shipping-option'</span>
<span class="hljs-keyword">import</span> { ShippingProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shipping-profile'</span>
<span class="hljs-keyword">import</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'./order'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Store <span class="hljs-keyword">extends</span> MedusaStore {
    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> User, <span class="hljs-function">(<span class="hljs-params">user</span>) =&gt;</span> user.store)
    members?: User[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Product, <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> product.store)
    products?: Product[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> ShippingOption, <span class="hljs-function">(<span class="hljs-params">shippingOption</span>) =&gt;</span> shippingOption.store)
    shippingOptions?: ShippingOption[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> ShippingProfile, <span class="hljs-function">(<span class="hljs-params">shippingProfile</span>) =&gt;</span> shippingProfile.store)
    shippingProfiles?: ShippingProfile[]

    <span class="hljs-comment">// ✅ Our missing relation</span>
    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Order, <span class="hljs-function">(<span class="hljs-params">order</span>) =&gt;</span> order.store)
    orders?: Order[]
}
</code></pre>
<h3 id="heading-create-the-migration">Create the migration</h3>
<p>Once the entity and repository have been extended, it's time for the migration :</p>
<pre><code class="lang-apache"><span class="hljs-attribute">npx</span> typeorm migration:create src/migrations/add-order-store-id-and-children
</code></pre>
<p>We can add thoses new features by updating the up and down functions :</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> up(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "order" ADD "store_id" character varying`</span>)
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`CREATE INDEX "OrderStoreId" ON "order" ("store_id")`</span>)

        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "order" ADD "order_parent_id" character varying`</span>)
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`CREATE INDEX "OrderParentId" ON "order" ("order_parent_id")`</span>)
    }

    <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> down(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`DROP INDEX "public"."OrderParentId"`</span>)
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "order" DROP COLUMN "order_parent_id"`</span>)

        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`DROP INDEX "public"."OrderStoreId"`</span>)
        <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "order" DROP COLUMN "store_id"`</span>)
    }
</code></pre>
<p>We can apply our changes by launching the server with <code>yarn build</code> and <code>npx medusa migrations run</code> .</p>
<h3 id="heading-extend-the-orderservice">Extend the <code>OrderService</code></h3>
<p>Ready to start integrating our features ? We'll make sure to only list the orders linked to a Store, and for the first time we'll also see how to prevent a Store from accessing an Order that do not belongs to it :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { FindConfig, OrderService <span class="hljs-keyword">as</span> MedusaOrderService, Selector } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">'awilix'</span>
<span class="hljs-keyword">import</span> { MedusaError } <span class="hljs-keyword">from</span> <span class="hljs-string">'medusa-core-utils'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/user'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/order'</span>

<span class="hljs-keyword">type</span> OrderSelector = {
    store_id?: <span class="hljs-built_in">string</span>
} &amp; Selector&lt;Order&gt;

<span class="hljs-keyword">class</span> OrderService <span class="hljs-keyword">extends</span> MedusaOrderService {
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.TRANSIENT

    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> loggedInUser_: User | <span class="hljs-literal">null</span>

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">container, options</span>) {
        <span class="hljs-comment">// @ts-ignore</span>
        <span class="hljs-built_in">super</span>(...arguments)

        <span class="hljs-keyword">try</span> {
            <span class="hljs-built_in">this</span>.loggedInUser_ = container.loggedInUser
        } <span class="hljs-keyword">catch</span> (e) {
            <span class="hljs-comment">// avoid errors when backend first runs</span>
        }
    }

    <span class="hljs-keyword">async</span> list(selector: OrderSelector, config?: FindConfig&lt;Order&gt;): <span class="hljs-built_in">Promise</span>&lt;Order[]&gt; {
        <span class="hljs-keyword">if</span> (!selector.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            selector.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        config.select?.push(<span class="hljs-string">'store_id'</span>)

        config.relations?.push(<span class="hljs-string">'store'</span>)

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.list(selector, config)
    }

    <span class="hljs-keyword">async</span> listAndCount(selector: OrderSelector, config?: FindConfig&lt;Order&gt;): <span class="hljs-built_in">Promise</span>&lt;[Order[], <span class="hljs-built_in">number</span>]&gt; {
        <span class="hljs-keyword">if</span> (!selector.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            selector.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        config.select?.push(<span class="hljs-string">'store_id'</span>)

        config.relations?.push(<span class="hljs-string">'store'</span>)

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.listAndCount(selector, config)
    }

    <span class="hljs-keyword">async</span> retrieve(orderId: <span class="hljs-built_in">string</span>, config: FindConfig&lt;Order&gt; = {}): <span class="hljs-built_in">Promise</span>&lt;Order&gt; {

        config.relations = [...(config.relations || []), <span class="hljs-string">'store'</span>]

        <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.retrieve(orderId, config)

        <span class="hljs-keyword">if</span> (order.store?.id &amp;&amp; 
            <span class="hljs-built_in">this</span>.loggedInUser_?.store_id &amp;&amp; 
            order.store.id !== <span class="hljs-built_in">this</span>.loggedInUser_.store_id) {
             <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(
                MedusaError.Types.NOT_FOUND,
                <span class="hljs-string">`Order with id <span class="hljs-subst">${orderId}</span> was not found`</span>
            )
        }

        <span class="hljs-keyword">return</span> order
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> OrderService
</code></pre>
<p>Three functions have been overrided in the above code. For the first two, if you've followed the previous sections, they simply add a <code>WHERE</code> condition to our SQL query to target a specific <code>store_id</code> if we have a logged in user with a store id.</p>
<p>For the <code>retrieve</code> function, which allows us to retrieve an order, here we retrieve the Order with its associated store (or not, in the case of a parent Order) with the original function.</p>
<p>And we take care to validate after we have the Order, to compare the store_id of the order with that of the logged-in user.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Don't forget that in our marketplace, if a user doesn't have a <code>store_id</code>, they considered admin.</div>
</div>

<h1 id="heading-create-orders-subscribers">Create Order's subscribers</h1>
<p>Ok so get ready, this part will be a little longer, but I assure you it's essential to your marketplace.</p>
<p>As I said at the beginning of this part, the idea is to split an order into several child orders, based on each store in an order.</p>
<p>For example, if the customer has created an order with 3 different products, all from a different store (3 different stores), we'd expect 3 different orders.</p>
<h2 id="heading-order-placed-event">Order Placed Event</h2>
<h3 id="heading-create-the-order-placed-subscriber">Create the Order placed subscriber</h3>
<p>Here, I'll create a folder inside the <code>src/subscribers</code> folder and name it <code>orders</code>, so that all subscribers linked to orders will be inside.</p>
<p>We can now create the <code>order-placed.ts</code> subscriber inside that new folder :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/subscribers/orders/order-placed.ts</span>

<span class="hljs-keyword">import</span> {
    Logger,
    OrderService,
    <span class="hljs-keyword">type</span> SubscriberArgs,
    <span class="hljs-keyword">type</span> SubscriberConfig
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { EntityManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleOrderPlaced</span>(<span class="hljs-params">{
    data,
    eventName,
    container,
    pluginOptions,
}: SubscriberArgs&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;</span>) </span>{
    <span class="hljs-keyword">const</span> manager = container.resolve&lt;EntityManager&gt;(<span class="hljs-string">'manager'</span>)
    <span class="hljs-keyword">const</span> logger = container.resolve&lt;Logger&gt;(<span class="hljs-string">'logger'</span>)

}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: SubscriberConfig = {
    event: OrderService.Events.PLACED,
    context: {
        subscriberId: <span class="hljs-string">'order-placed-handler'</span>,
    },
}
</code></pre>
<p>For the moment, we're going to keep things very simple and simply have this piece of code that we'll feed in as we go along.</p>
<p>By the way, it reminds me that this is the first time we're going to use the <code>manager</code>.</p>
<p>The manager will enable us to create a transaction, which will be very useful here and we have already used in the previous part quickly.</p>
<p><em>A transaction is a way to ensure that multiple database operations are executed as a single atomic unit of work.</em> This means that either all operations succeed and are committed to the database, or if any operation fails, all operations are rolled back and the database remains unchanged (Pretty cool, right?)</p>
<h3 id="heading-retrieve-the-order-placed">Retrieve the Order placed</h3>
<p>First, we'll retrieve the order that has just been created and intercepted by our subscriber :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ... rest for size purposes</span>
<span class="hljs-keyword">await</span> manager.transaction(<span class="hljs-keyword">async</span> (m) =&gt; {
    <span class="hljs-keyword">const</span> orderService: OrderService = container.resolve&lt;OrderService&gt;(<span class="hljs-string">'orderService'</span>)
    <span class="hljs-keyword">const</span> productService: ProductService = container.resolve&lt;ProductService&gt;(<span class="hljs-string">'productService'</span>)
    <span class="hljs-keyword">const</span> shippingOptionService: ShippingOptionService =
        container.resolve&lt;ShippingOptionService&gt;(<span class="hljs-string">'shippingOptionService'</span>)

    <span class="hljs-keyword">const</span> orderRepo = m.getRepository(Order)
    <span class="hljs-keyword">const</span> lineItemRepo = m.getRepository(LineItem)
    <span class="hljs-keyword">const</span> shippingMethodRepo = m.getRepository(ShippingMethod)

    <span class="hljs-keyword">const</span> orderActivity = logger.activity(<span class="hljs-string">`Splitting order <span class="hljs-subst">${data.id}</span> into child orders...`</span>)

    <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> orderService.retrieve(data.id, {
        relations: [<span class="hljs-string">'items'</span>, <span class="hljs-string">'items.variant'</span>, <span class="hljs-string">'items.variant.prices'</span>, <span class="hljs-string">'cart'</span>, <span class="hljs-string">'payments'</span>, <span class="hljs-string">'shipping_methods'</span>],
    })

    <span class="hljs-keyword">if</span> (!order) {
        logger.failure(orderActivity, <span class="hljs-string">`OrderPlacedSubscriber | Order not found for order id <span class="hljs-subst">${data.id}</span>.`</span>)
        <span class="hljs-keyword">return</span>
    }
   <span class="hljs-comment">// We have successfully retrieved the Order,</span>
   <span class="hljs-comment">// we'll add our next logic here.</span>

})

<span class="hljs-comment">// ... rest for size purposes</span>
</code></pre>
<h3 id="heading-grouping-lineitems-by-store">Grouping LineItems by Store</h3>
<p>It's easier to group products by store so you know how many distinct stores there are, and it will make creating a child order easier :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (!order) {
    logger.failure(orderActivity, <span class="hljs-string">`OrderPlacedSubscriber | Order not found for order id <span class="hljs-subst">${data.id}</span>.`</span>)
    <span class="hljs-keyword">return</span>
}

<span class="hljs-comment">// #1 : Group Items By Store Id</span>

<span class="hljs-keyword">const</span> storesWithItems = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">string</span>, Order[<span class="hljs-string">'items'</span>]&gt;()

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> order.items) {
    <span class="hljs-keyword">const</span> product: Product = <span class="hljs-keyword">await</span> productService.retrieve(item.variant.product_id)
    <span class="hljs-keyword">const</span> storeId = product.store_id

    <span class="hljs-keyword">if</span> (!storeId) {
        logger.failure(orderActivity, <span class="hljs-string">`OrderPlacedSubscriber | product.store_id not found for product <span class="hljs-subst">${product.id}</span>.`</span>)
        <span class="hljs-keyword">continue</span>
    }

    <span class="hljs-keyword">if</span> (!storesWithItems.has(storeId)) {
        storesWithItems.set(storeId, [])
    }

    storesWithItems.get(storeId).push(item)
}
</code></pre>
<h3 id="heading-creating-a-child-order-for-each-store">Creating a child Order for each store</h3>
<p>Just below the code above, we will begin a loop on each item in our Map to construct a child order for each of the shops, including all of the LineItems indicated above :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// #2 : For each store, create a new order with the relevant items and shipping methods</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> [storeId, items] <span class="hljs-keyword">of</span> storesWithItems.entries()) {

    <span class="hljs-comment">// #2.1 : Create a new order</span>
    <span class="hljs-keyword">const</span> childOrder = orderRepo.create({
        ...order,
        order_parent_id: order.id,
        store_id: storeId,
        cart_id: <span class="hljs-literal">null</span>,
        cart: <span class="hljs-literal">null</span>,
        id: <span class="hljs-literal">null</span>,
        shipping_methods: [],
    })

    <span class="hljs-keyword">const</span> savedChildOrder = <span class="hljs-keyword">await</span> orderRepo.save(childOrder)

    <span class="hljs-comment">// #2.2 : Create a new line item for each item in the order</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> items) {
        <span class="hljs-keyword">const</span> lineItem = lineItemRepo.create({
            ...item,
            order_id: savedChildOrder.id,
            cart_id: <span class="hljs-literal">null</span>,
            id: <span class="hljs-literal">null</span>,
        })
        <span class="hljs-keyword">await</span> lineItemRepo.save(lineItem)
    }

   <span class="hljs-comment">// Do not close the for loop yet.</span>
</code></pre>
<h3 id="heading-create-a-shippingmethod-for-each-child-order">Create a ShippingMethod for each child order</h3>
<p>Still inside the loop, we'll create a ShippingMethod for each store, so that each store can manage the progress of the delivery independently. If you don't know the difference between a ShippingMethod and a ShippingOption, <a target="_blank" href="https://docs.medusajs.com/modules/carts-and-checkout/shipping#shipping-method-purpose">I invite you to read this part of the documentation</a></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// #2.3 : Create a new shipping method for each child order with a matching shipping option that is in the same store</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> shippingMethod <span class="hljs-keyword">of</span> order.shipping_methods) {
    <span class="hljs-keyword">const</span> shippingOption = <span class="hljs-keyword">await</span> shippingOptionService.retrieve(shippingMethod.shipping_option_id)

    <span class="hljs-keyword">if</span> (shippingOption.store_id !== storeId) {
        <span class="hljs-keyword">continue</span>
    }

    <span class="hljs-keyword">const</span> newShippingMethod = shippingMethodRepo.create({
        ...shippingMethod,
        id: <span class="hljs-literal">null</span>,
        cart_id: <span class="hljs-literal">null</span>,
        cart: <span class="hljs-literal">null</span>,
        order_id: savedChildOrder.id,
    })
    <span class="hljs-keyword">await</span> shippingMethodRepo.save(newShippingMethod)
}
</code></pre>
<h3 id="heading-complete-implementation">Complete implementation</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/subscribers/orders/order-placed.ts</span>
<span class="hljs-keyword">import</span> {
    LineItem,
    Logger,
    OrderService,
    ShippingMethod,
    <span class="hljs-keyword">type</span> SubscriberArgs,
    <span class="hljs-keyword">type</span> SubscriberConfig
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> ShippingOptionService <span class="hljs-keyword">from</span> <span class="hljs-string">'src/services/shipping-option'</span>
<span class="hljs-keyword">import</span> { EntityManager } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>
<span class="hljs-keyword">import</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../models/order'</span>
<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../models/product'</span>
<span class="hljs-keyword">import</span> ProductService <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/product'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleOrderPlaced</span>(<span class="hljs-params">{
    data,
    eventName,
    container,
    pluginOptions,
}: SubscriberArgs&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;</span>) </span>{
    <span class="hljs-keyword">const</span> manager = container.resolve&lt;EntityManager&gt;(<span class="hljs-string">'manager'</span>)
    <span class="hljs-keyword">const</span> logger = container.resolve&lt;Logger&gt;(<span class="hljs-string">'logger'</span>)

    <span class="hljs-keyword">await</span> manager.transaction(<span class="hljs-keyword">async</span> (m) =&gt; {
        <span class="hljs-keyword">const</span> orderService: OrderService = container.resolve&lt;OrderService&gt;(<span class="hljs-string">'orderService'</span>)
        <span class="hljs-keyword">const</span> productService: ProductService = container.resolve&lt;ProductService&gt;(<span class="hljs-string">'productService'</span>)
        <span class="hljs-keyword">const</span> shippingOptionService: ShippingOptionService =
            container.resolve&lt;ShippingOptionService&gt;(<span class="hljs-string">'shippingOptionService'</span>)

        <span class="hljs-keyword">const</span> orderRepo = m.getRepository(Order)
        <span class="hljs-keyword">const</span> lineItemRepo = m.getRepository(LineItem)
        <span class="hljs-keyword">const</span> shippingMethodRepo = m.getRepository(ShippingMethod)

        <span class="hljs-keyword">const</span> orderActivity = logger.activity(<span class="hljs-string">`Splitting order <span class="hljs-subst">${data.id}</span> into child orders...`</span>)

        <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> orderService.retrieve(data.id, {
            relations: [<span class="hljs-string">'items'</span>, <span class="hljs-string">'items.variant'</span>, <span class="hljs-string">'items.variant.prices'</span>, <span class="hljs-string">'cart'</span>, <span class="hljs-string">'payments'</span>, <span class="hljs-string">'shipping_methods'</span>],
        })

        <span class="hljs-keyword">if</span> (!order) {
            logger.failure(orderActivity, <span class="hljs-string">`OrderPlacedSubscriber | Order not found for order id <span class="hljs-subst">${data.id}</span>.`</span>)
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-comment">// #1 : Group Items By Store Id</span>

        <span class="hljs-keyword">const</span> storesWithItems = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">string</span>, Order[<span class="hljs-string">'items'</span>]&gt;()

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> order.items) {
            <span class="hljs-keyword">const</span> product: Product = <span class="hljs-keyword">await</span> productService.retrieve(item.variant.product_id)
            <span class="hljs-keyword">const</span> storeId = product.store_id

            <span class="hljs-keyword">if</span> (!storeId) {
                logger.failure(orderActivity, <span class="hljs-string">`OrderPlacedSubscriber | product.store_id not found for product <span class="hljs-subst">${product.id}</span>.`</span>)
                <span class="hljs-keyword">continue</span>
            }

            <span class="hljs-keyword">if</span> (!storesWithItems.has(storeId)) {
                storesWithItems.set(storeId, [])
            }

            storesWithItems.get(storeId).push(item)
        }

        <span class="hljs-comment">// #2 : For each store, create a new order with the relevant items and shipping methods</span>
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> [storeId, items] <span class="hljs-keyword">of</span> storesWithItems.entries()) {

            <span class="hljs-comment">// #2.1 : Create a new order</span>
            <span class="hljs-keyword">const</span> childOrder = orderRepo.create({
                ...order,
                order_parent_id: order.id,
                store_id: storeId,
                cart_id: <span class="hljs-literal">null</span>,
                cart: <span class="hljs-literal">null</span>,
                id: <span class="hljs-literal">null</span>,
                shipping_methods: [],
            })

            <span class="hljs-keyword">const</span> savedChildOrder = <span class="hljs-keyword">await</span> orderRepo.save(childOrder)

            <span class="hljs-comment">// #2.2 : Create a new line item for each item in the order</span>
            <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> items) {
                <span class="hljs-keyword">const</span> lineItem = lineItemRepo.create({
                    ...item,
                    order_id: savedChildOrder.id,
                    cart_id: <span class="hljs-literal">null</span>,
                    id: <span class="hljs-literal">null</span>,
                })
                <span class="hljs-keyword">await</span> lineItemRepo.save(lineItem)
            }

            <span class="hljs-comment">// #2.3 : Create a new shipping method for each child order with a matching shipping option that is in the same store</span>
            <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> shippingMethod <span class="hljs-keyword">of</span> order.shipping_methods) {
                <span class="hljs-keyword">const</span> shippingOption = <span class="hljs-keyword">await</span> shippingOptionService.retrieve(shippingMethod.shipping_option_id)

                <span class="hljs-keyword">if</span> (shippingOption.store_id !== storeId) {
                    <span class="hljs-keyword">continue</span>
                }

                <span class="hljs-keyword">const</span> newShippingMethod = shippingMethodRepo.create({
                    ...shippingMethod,
                    id: <span class="hljs-literal">null</span>,
                    cart_id: <span class="hljs-literal">null</span>,
                    cart: <span class="hljs-literal">null</span>,
                    order_id: savedChildOrder.id,
                })
                <span class="hljs-keyword">await</span> shippingMethodRepo.save(newShippingMethod)
            }
        }

        logger.success(orderActivity, <span class="hljs-string">`OrderPlacedSubscriber | Order <span class="hljs-subst">${data.id}</span> has been split into <span class="hljs-subst">${storesWithItems.size}</span> child orders.`</span>)
    })
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: SubscriberConfig = {
    event: OrderService.Events.PLACED,
    context: {
        subscriberId: <span class="hljs-string">'order-placed-handler'</span>,
    },
}
</code></pre>
<p>Perfect! We now have a way of splitting an Order into several child Orders!</p>
<p>However, we still need to listen to other Order events.</p>
<h2 id="heading-order-updated-event">Order Updated Event</h2>
<p>Here, we're going to intercept an order whenever it has been updated. The idea is to be able to update the parent Order according to the status of its children :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    FulfillmentStatus,
    OrderStatus,
    PaymentStatus,
    <span class="hljs-keyword">type</span> SubscriberArgs,
    <span class="hljs-keyword">type</span> SubscriberConfig,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> OrderRepository <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa/dist/repositories/order'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../models/order'</span>
<span class="hljs-keyword">import</span> OrderService <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/order'</span>


<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleOrderUpdated</span>(<span class="hljs-params">{
    data,
    eventName,
    container,
    pluginOptions,
}: SubscriberArgs&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;</span>) </span>{
    <span class="hljs-keyword">const</span> orderService: OrderService = container.resolve(<span class="hljs-string">'orderService'</span>)
    <span class="hljs-keyword">const</span> orderRepo: <span class="hljs-keyword">typeof</span> OrderRepository = container.resolve(<span class="hljs-string">'orderRepository'</span>)

    <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> orderService.retrieve(data.id)

    <span class="hljs-keyword">if</span> (!order.order_parent_id) {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">const</span> parentOrder = <span class="hljs-keyword">await</span> orderService.retrieve(order.order_parent_id, {
        relations: [<span class="hljs-string">'children'</span>],
    })

    <span class="hljs-keyword">const</span> status = <span class="hljs-keyword">await</span> getStatusFromChildren(parentOrder)

    <span class="hljs-keyword">if</span> (status !== parentOrder.status) {
        <span class="hljs-keyword">switch</span> (status) {
            <span class="hljs-keyword">case</span> OrderStatus.CANCELED:
                <span class="hljs-keyword">await</span> orderService.cancel(parentOrder.id)
            <span class="hljs-keyword">case</span> OrderStatus.ARCHIVED:
                <span class="hljs-keyword">await</span> orderService.archive(parentOrder.id)
            <span class="hljs-keyword">case</span> OrderStatus.COMPLETED:
                <span class="hljs-keyword">await</span> orderService.completeOrder(parentOrder.id)
            <span class="hljs-keyword">default</span>:
                parentOrder.status = status
                parentOrder.fulfillment_status = (status === <span class="hljs-string">'completed'</span>
                    ? FulfillmentStatus.SHIPPED
                    : status) <span class="hljs-keyword">as</span> unknown <span class="hljs-keyword">as</span> FulfillmentStatus
                parentOrder.payment_status = (status === <span class="hljs-string">'completed'</span>) ? PaymentStatus.CAPTURED : status <span class="hljs-keyword">as</span> unknown <span class="hljs-keyword">as</span> PaymentStatus
                <span class="hljs-keyword">await</span> orderRepo.save(parentOrder)
        }
    }
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getStatusFromChildren</span>(<span class="hljs-params">order: Order</span>) </span>{
    <span class="hljs-keyword">if</span> (!order.children) {
        <span class="hljs-keyword">return</span> order.status
    }

    <span class="hljs-keyword">let</span> statuses = order.children.map(<span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span> child.status)
    <span class="hljs-comment">//remove duplicate statuses</span>
    statuses = [...new <span class="hljs-built_in">Set</span>(statuses)]

    <span class="hljs-keyword">if</span> (statuses.length === <span class="hljs-number">1</span>) {
        <span class="hljs-keyword">return</span> statuses[<span class="hljs-number">0</span>]
    }

    statuses = statuses.filter(<span class="hljs-function">(<span class="hljs-params">status</span>) =&gt;</span> status !== OrderStatus.CANCELED &amp;&amp; status !== OrderStatus.ARCHIVED)

    <span class="hljs-keyword">if</span> (!statuses.length) {
        <span class="hljs-comment">//all child orders are archived or canceled</span>
        <span class="hljs-keyword">return</span> OrderStatus.CANCELED
    }

    <span class="hljs-keyword">if</span> (statuses.length === <span class="hljs-number">1</span>) {
        <span class="hljs-keyword">return</span> statuses[<span class="hljs-number">0</span>]
    }

    <span class="hljs-comment">//check if any order requires action</span>
    <span class="hljs-keyword">const</span> hasRequiresAction = statuses.some(<span class="hljs-function">(<span class="hljs-params">status</span>) =&gt;</span> status === OrderStatus.REQUIRES_ACTION)

    <span class="hljs-keyword">if</span> (hasRequiresAction) {
        <span class="hljs-keyword">return</span> OrderStatus.REQUIRES_ACTION
    }

    <span class="hljs-comment">//since more than one status is left and we filtered out canceled, archived,</span>
    <span class="hljs-comment">//and requires action statuses, only pending and complete left. So, return pending</span>
    <span class="hljs-keyword">return</span> OrderStatus.PENDING
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: SubscriberConfig = {
    event: [
        OrderService.Events.UPDATED,
        OrderService.Events.FULFILLMENT_CREATED,
        OrderService.Events.FULFILLMENT_CANCELED,
        OrderService.Events.GIFT_CARD_CREATED,
        OrderService.Events.ITEMS_RETURNED,
        OrderService.Events.PAYMENT_CAPTURED,
        OrderService.Events.PAYMENT_CAPTURE_FAILED,
        OrderService.Events.REFUND_CREATED,
        OrderService.Events.REFUND_FAILED,
        OrderService.Events.RETURN_ACTION_REQUIRED,
        OrderService.Events.RETURN_REQUESTED,
        OrderService.Events.SHIPMENT_CREATED,
        OrderService.Events.SWAP_CREATED,
    ],
    context: {
        subscriberId: <span class="hljs-string">'order-updated-handler'</span>,
    },
}
</code></pre>
<h3 id="heading-create-a-subscriber-that-will-update-the-child-orders-status">Create a subscriber that will update the child order's status</h3>
<p>Medusa doesn't implement any predefined conditions for setting the status of an order to <code>completed</code>, in our example below.</p>
<p>In our example below, we'll set the status of an order to “complete” when :</p>
<ul>
<li><p>This order has a payment_status of <code>completed</code>.</p>
</li>
<li><p>This order has a fulfillment_status of <code>completed</code>.</p>
</li>
<li><p>We'll also make sure that the order in question hasn't been <code>canceled</code>, <code>archived</code> or already <code>completed</code></p>
</li>
</ul>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is an example, you can of course adjust to your needs.</div>
</div>

<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    <span class="hljs-keyword">type</span> SubscriberConfig,
    <span class="hljs-keyword">type</span> SubscriberArgs,
    FulfillmentStatus,
    PaymentStatus,
    OrderStatus,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> OrderService <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/order'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../models/order'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleOrderUpdated</span>(<span class="hljs-params">{
    data,
    eventName,
    container,
    pluginOptions,
}: SubscriberArgs&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;</span>) </span>{
    <span class="hljs-keyword">const</span> orderService: OrderService = container.resolve(<span class="hljs-string">'orderService'</span>)

    <span class="hljs-keyword">const</span> order = <span class="hljs-keyword">await</span> orderService.retrieve(data.id)

    <span class="hljs-keyword">if</span> (!order.order_parent_id) {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">const</span> parentOrder = <span class="hljs-keyword">await</span> orderService.retrieve(order.order_parent_id, {
        relations: [<span class="hljs-string">'children'</span>],
    })

    <span class="hljs-keyword">await</span> updateStatusOfChildren(parentOrder, orderService)
}

<span class="hljs-comment">/**
 * This function is executed when a child order is updated.
 * It checks if the child order has a payment status of "captured" and a fulfillment status of "shipped".
 * If both conditions are met, it updates the child order's status to "complete", allowing a parent order to be marked as "complete" too.
 */</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updateStatusOfChildren</span>(<span class="hljs-params">order: Order, orderService: OrderService</span>) </span>{
    <span class="hljs-keyword">if</span> (!order.children) {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">const</span> ordersToComplete = order.children
        .filter(<span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span> child.payment_status === PaymentStatus.CAPTURED)
        .filter(<span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span> child.fulfillment_status === FulfillmentStatus.SHIPPED)
        .filter(
            <span class="hljs-function">(<span class="hljs-params">child</span>) =&gt;</span>
                child.status !== OrderStatus.CANCELED &amp;&amp;
                child.status !== OrderStatus.ARCHIVED &amp;&amp;
                child.status !== OrderStatus.COMPLETED,
        )

    <span class="hljs-keyword">if</span> (ordersToComplete.length === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> order <span class="hljs-keyword">of</span> ordersToComplete) {
        <span class="hljs-keyword">await</span> orderService.completeOrder(order.id)
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: SubscriberConfig = {
    event: [
        OrderService.Events.UPDATED,
        OrderService.Events.FULFILLMENT_CREATED,
        OrderService.Events.FULFILLMENT_CANCELED,
        OrderService.Events.GIFT_CARD_CREATED,
        OrderService.Events.ITEMS_RETURNED,
        OrderService.Events.PAYMENT_CAPTURED,
        OrderService.Events.PAYMENT_CAPTURE_FAILED,
        OrderService.Events.REFUND_CREATED,
        OrderService.Events.REFUND_FAILED,
        OrderService.Events.RETURN_ACTION_REQUIRED,
        OrderService.Events.RETURN_REQUESTED,
        OrderService.Events.SHIPMENT_CREATED,
        OrderService.Events.SWAP_CREATED,
    ],
    context: {
        subscriberId: <span class="hljs-string">'child-order-updated-handler'</span>,
    },
}
</code></pre>
<p>And now you've successfully implemented the splitting of one order into several for each Store! Granted, this part of the program was rather heavy and involved a lot of different concepts, so don't hesitate to take the time to reread the code and try to understand the integrated flow.</p>
<p>If you have any questions, you can send me a DM on <a target="_blank" href="https://x.com/adevinwild">Twitter/X</a> or on the <a target="_blank" href="https://discord.gg/medusajs">official Medusa Discord!</a></p>
<h2 id="heading-whats-next">What's Next ?</h2>
<p>The next part will be a <em>little special</em>, we'll be looking at <strong>Payments entity</strong>, and we'll be extending it, but this will be more of an exercise for you, of course the solution will also be included, but it might be nice to practice customizing an entity on your own.</p>
<h2 id="heading-common-issues">Common Issues</h2>
<p><strong>I have types errors!</strong></p>
<p>Do not forget to update you <code>index.d.ts</code> file created earlier, at this point, and what we have used, you should have an <code>index.d.ts</code> file that looks like this :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/index.d.ts</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'./models/product'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ShippingProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">'./models/shipping-profile'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ShippingOption } <span class="hljs-keyword">from</span> <span class="hljs-string">'./models/shipping-option'</span> 
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Order } <span class="hljs-keyword">from</span> <span class="hljs-string">'./models/shipping-order'</span> 

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">module</span> '@medusajs/medusa/dist/models/product' {
    <span class="hljs-keyword">interface</span> Product {
        store_id?: <span class="hljs-built_in">string</span>
        store?: Store
    }
}

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">module</span> '@medusajs/medusa/dist/models/shipping-profile' {
    <span class="hljs-keyword">interface</span> ShippingProfile {
        store_id?: <span class="hljs-built_in">string</span>
        store?: Store
    }
}

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">module</span> '@medusajs/medusa/dist/models/shipping-option' {
    <span class="hljs-keyword">interface</span> ShippingOption {
        store_id?: <span class="hljs-built_in">string</span>
        store?: Store
    }
}

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">module</span> '@medusajs/medusa/dist/models/order' {
    <span class="hljs-keyword">interface</span> Order {
        store_id?: <span class="hljs-built_in">string</span>
        store?: Store
        order_parent_id?: <span class="hljs-built_in">string</span>
        parent?: Order
        children?: Order[]
    }
}
</code></pre>
<h2 id="heading-github-branch">GitHub Branch</h2>
<p>You can access the complete part's code <a target="_blank" href="https://git.new/perseides-part-3"><strong>here</strong></a><strong>.</strong></p>
<h2 id="heading-contact">Contact</h2>
<p>You can contact me on Discord and X with the same username : <a target="_blank" href="https://twitter.com/adevinwild">@adevinwild</a></p>
]]></content:encoded></item><item><title><![CDATA[Medusa Marketplace #2.4 | Extending Shipping services]]></title><description><![CDATA[Hello everyone!
Now that we've extended the ShippingOption and ShippingProfile entities, we can start integrating new features into our services.
What's the goal here ?
The aim of this part is to override various functions of the ShippingOption and S...]]></description><link>https://blog.perseides.org/medusa-marketplace-24-extending-shipping-services</link><guid isPermaLink="true">https://blog.perseides.org/medusa-marketplace-24-extending-shipping-services</guid><category><![CDATA[medusa.js]]></category><category><![CDATA[medusa]]></category><category><![CDATA[course]]></category><category><![CDATA[Series]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[marketplace]]></category><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Fri, 10 May 2024 09:00:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715245096433/04c5f6c0-ea01-4f17-8e6e-83f0ccd9b39f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Hello everyone!</strong></p>
<p>Now that we've extended the ShippingOption and ShippingProfile entities, we can start integrating new features into our services.</p>
<h3 id="heading-whats-the-goal-here">What's the goal here ?</h3>
<p>The aim of this part is to override various functions of the ShippingOption and ShippingProfile services, so that vendors can only access their own shipping options/profiles, and create a shipping option/profile for their store only.</p>
<p>We'll also look at the concept of loaders, to create a default ShippingProfile for each store.</p>
<h2 id="heading-shippingoptionservice">ShippingOptionService</h2>
<h3 id="heading-extend-the-service">Extend the service</h3>
<p>Let's start by creating the ShippingOptionService file :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/shipping-option.ts</span>
<span class="hljs-keyword">import</span> { ShippingOptionService <span class="hljs-keyword">as</span> MedusaShippingOptionService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">'awilix'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/user'</span>

<span class="hljs-keyword">class</span> ShippingOptionService <span class="hljs-keyword">extends</span> MedusaShippingOptionService {
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.TRANSIENT
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> loggedInUser_: User | <span class="hljs-literal">null</span>

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">container, options</span>) {
        <span class="hljs-comment">// @ts-ignore</span>
        <span class="hljs-built_in">super</span>(...arguments)

        <span class="hljs-keyword">try</span> {
            <span class="hljs-built_in">this</span>.loggedInUser_ = container.loggedInUser
        } <span class="hljs-keyword">catch</span> (e) {
            <span class="hljs-comment">// avoid errors when backend first runs</span>
        }
    }

}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ShippingOptionService
</code></pre>
<h3 id="heading-override-the-shippingoptionservicelist-function">Override the <code>ShippingOptionService.list</code> function</h3>
<p>We will now override the <code>ShippingOptionService.list</code> function and the <code>ShippingOptionService.listAndCount</code> function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ShippingOptionService <span class="hljs-keyword">as</span> MedusaShippingOptionService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">'awilix'</span>

<span class="hljs-keyword">import</span> { FindConfig, Selector } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ShippingOption } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/shipping-option'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/user'</span>

<span class="hljs-keyword">type</span> ShippingOptionSelector = {
    store_id?: <span class="hljs-built_in">string</span>
} &amp; Selector&lt;ShippingOption&gt;

<span class="hljs-keyword">class</span> ShippingOptionService <span class="hljs-keyword">extends</span> MedusaShippingOptionService {   
     <span class="hljs-comment">// ... rest</span>

    <span class="hljs-keyword">async</span> list(
        selector?: ShippingOptionSelector &amp; { q?: <span class="hljs-built_in">string</span> },
        config?: FindConfig&lt;ShippingOption&gt;,
    ): <span class="hljs-built_in">Promise</span>&lt;ShippingOption[]&gt; {
        <span class="hljs-keyword">if</span> (!selector.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            selector.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        config.select?.push(<span class="hljs-string">'store_id'</span>)

        config.relations?.push(<span class="hljs-string">'store'</span>)

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.list(selector, config)
    }

    <span class="hljs-keyword">async</span> listAndCount(
        selector?: ShippingOptionSelector &amp; { q?: <span class="hljs-built_in">string</span> },
        config?: FindConfig&lt;ShippingOption&gt;,
    ): <span class="hljs-built_in">Promise</span>&lt;[ShippingOption[], <span class="hljs-built_in">number</span>]&gt; {
        <span class="hljs-keyword">if</span> (!selector.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            selector.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        config.select?.push(<span class="hljs-string">'store_id'</span>)

        config.relations?.push(<span class="hljs-string">'store'</span>)

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.listAndCount(selector, config)
    }
}
</code></pre>
<h3 id="heading-override-the-shippingoptionservicecreate-function">Override the <code>ShippingOptionService.create</code> function</h3>
<p>We also override the <code>ShippingOptionService.create</code> function and the <code>CreateShippingOptionInput</code> type to allow for a <code>store_id</code> property :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ... other imports</span>
<span class="hljs-keyword">import</span> { CreateShippingOptionInput <span class="hljs-keyword">as</span> MedusaCreateShippingOptionInput } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa/dist/types/shipping-options'</span>

<span class="hljs-keyword">type</span> CreateShippingOptionInput = {
    store_id?: <span class="hljs-built_in">string</span>
} &amp; MedusaCreateShippingOptionInput

<span class="hljs-keyword">class</span> ShippingOptionService <span class="hljs-keyword">extends</span> MedusaShippingOptionService {
    <span class="hljs-comment">// ... rest </span>
   <span class="hljs-keyword">async</span> create(data: CreateShippingOptionInput): <span class="hljs-built_in">Promise</span>&lt;ShippingOption&gt; {
        <span class="hljs-keyword">if</span> (!data.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            data.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.create(data)
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ShippingOptionService
</code></pre>
<h3 id="heading-override-the-shippingoptionservicevalidatecartoption">Override the <code>ShippingOptionService.validateCartOption</code></h3>
<p>A final function to be overrided is the <code>validateCartOption</code> function, which validates a ShippingOption for a Cart before saving it in the Cart. Here, we're going to make sure that a chosen shipping option, has one or more products that belongs to the same store :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">class</span> ShippingOptionService <span class="hljs-keyword">extends</span> MedusaShippingOptionService {
    <span class="hljs-comment">// ... rest</span>
    <span class="hljs-keyword">async</span> validateCartOption(option: ShippingOption, cart: Cart): <span class="hljs-built_in">Promise</span>&lt;ShippingOption | <span class="hljs-literal">null</span>&gt; {
        <span class="hljs-keyword">const</span> validatedOption = <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.validateCartOption(option, cart)

        <span class="hljs-keyword">const</span> hasAnItemFromStore = cart.items.some(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.variant.product.store_id === option.store_id)

        <span class="hljs-keyword">if</span> (!hasAnItemFromStore) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> MedusaError(MedusaError.Types.NOT_ALLOWED, <span class="hljs-string">'Shipping option does not exist for store'</span>)
        }

        <span class="hljs-keyword">return</span> validatedOption
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ShippingOptionService
</code></pre>
<h2 id="heading-shippingprofileservice">ShippingProfileService</h2>
<h3 id="heading-extend-the-service-1">Extend the service</h3>
<p>Before launching our server and starting to create ShippingOption, we need to extend ShippingProfileService :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    ShippingProfileService <span class="hljs-keyword">as</span> MedusaShippingProfileService
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">'awilix'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/user'</span>


<span class="hljs-keyword">class</span> ShippingProfileService <span class="hljs-keyword">extends</span> MedusaShippingProfileService {
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.TRANSIENT
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> loggedInUser_: User | <span class="hljs-literal">null</span>

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">container, options</span>) {
        <span class="hljs-comment">// @ts-ignore</span>
        <span class="hljs-built_in">super</span>(...arguments)

        <span class="hljs-keyword">try</span> {
            <span class="hljs-built_in">this</span>.loggedInUser_ = container.loggedInUser
        } <span class="hljs-keyword">catch</span> (e) {
            <span class="hljs-comment">// avoid errors when backend first runs</span>
        }
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ShippingProfileService
</code></pre>
<h3 id="heading-override-the-shippingprofileservicelist">Override the <code>ShippingProfileService.list</code></h3>
<p>By overriding this function, we can ensure that only the ShippingProfiles of the connected user's store are listed:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    FindConfig,
    ShippingProfileService <span class="hljs-keyword">as</span> MedusaShippingProfileService,
    Selector
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">'awilix'</span>

<span class="hljs-keyword">import</span> { ShippingProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/shipping-profile'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/user'</span>

<span class="hljs-keyword">type</span> ShippingProfileSelector = {
    store_id?: <span class="hljs-built_in">string</span>
} &amp; Selector&lt;ShippingProfile&gt;

<span class="hljs-keyword">class</span> ShippingProfileService <span class="hljs-keyword">extends</span> MedusaShippingProfileService {
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.TRANSIENT
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> loggedInUser_: User | <span class="hljs-literal">null</span>

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">container, options</span>) {
        <span class="hljs-comment">// @ts-ignore</span>
        <span class="hljs-built_in">super</span>(...arguments)

        <span class="hljs-keyword">try</span> {
            <span class="hljs-built_in">this</span>.loggedInUser_ = container.loggedInUser
        } <span class="hljs-keyword">catch</span> (e) {
            <span class="hljs-comment">//  avoid errors when backend first runs</span>
        }
    }

    <span class="hljs-keyword">async</span> list(
        selector: ShippingProfileSelector = {},
        config: FindConfig&lt;ShippingProfile&gt; = {}
    ): <span class="hljs-built_in">Promise</span>&lt;ShippingProfile[]&gt; {
        <span class="hljs-keyword">if</span> (!selector.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id){
            selector.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        config.relations = [...(config.relations ?? []), <span class="hljs-string">'store'</span>];

        config.select = [
            ...(config.select ?? []),
            <span class="hljs-string">'id'</span>,
            <span class="hljs-string">'name'</span>,
            <span class="hljs-string">'created_at'</span>,
            <span class="hljs-string">'deleted_at'</span>,
            <span class="hljs-string">'updated_at'</span>,
            <span class="hljs-string">'type'</span>,
            <span class="hljs-string">'store_id'</span>,
            <span class="hljs-string">'metadata'</span>,
        ];

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.list(selector, config);
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ShippingProfileService
</code></pre>
<h3 id="heading-override-the-shippingprofileservicecreate">Override the <code>ShippingProfileService.create</code></h3>
<p>When creating a ShippingProfile, if a user is logged in, we can assign its <code>store_id</code> :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ...imports</span>
<span class="hljs-keyword">import</span> { CreateShippingProfile <span class="hljs-keyword">as</span> MedusaCreateShippingProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa/dist/types/shipping-profile'</span>

<span class="hljs-keyword">type</span> CreateShippingProfile = {
    store_id?: <span class="hljs-built_in">string</span>
} &amp; MedusaCreateShippingProfile

<span class="hljs-keyword">class</span> ShippingProfileService <span class="hljs-keyword">extends</span> MedusaShippingProfileService { 
    <span class="hljs-comment">// ...rest</span>
    <span class="hljs-keyword">async</span> create(profile: CreateShippingProfile): <span class="hljs-built_in">Promise</span>&lt;ShippingProfile&gt; {
        <span class="hljs-keyword">if</span> (!profile.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            profile.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.create(profile)
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ShippingProfileService
</code></pre>
<h3 id="heading-override-the-shippingprofileretrievedefault-function">Override the <code>ShippingProfile.retrieveDefault</code> function</h3>
<p>Overriding this function allows for new created products being linked to the store's shipping profile, this is useful for the storefront, since it will fetch Cart options depending on the shipping profile of the current products in the Cart :</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">async</span> retrieveDefault(): <span class="hljs-built_in">Promise</span>&lt;ShippingProfile&gt; {
        <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.shippingProfileRepository_.findOne({
                where: {
                    <span class="hljs-keyword">type</span>: ShippingProfileType.CUSTOM,
                    store_id: <span class="hljs-built_in">this</span>.loggedInUser_.store_id
                }
            })
        }

        <span class="hljs-keyword">return</span> <span class="hljs-built_in">super</span>.retrieveDefault()
    }
</code></pre>
<h3 id="heading-create-the-shippingprofilecreatedefaultforstore-function">Create the <code>ShippingProfile.createDefaultForStore</code> function</h3>
<p>This function will later allow us to create default ShippingProfile for our stores:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">class</span> ShippingProfileService <span class="hljs-keyword">extends</span> MedusaShippingProfileService { 
    <span class="hljs-comment">// ...rest</span>
    <span class="hljs-keyword">async</span> createDefaultForStore(storeId: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;ShippingProfile&gt; {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.atomicPhase_(<span class="hljs-keyword">async</span> (manager) =&gt; {
            <span class="hljs-keyword">const</span> profileRepository = manager.withRepository(<span class="hljs-built_in">this</span>.shippingProfileRepository_)

            <span class="hljs-keyword">const</span> profile = <span class="hljs-keyword">await</span> profileRepository.findOne({ where: { store_id: storeId } })
            <span class="hljs-keyword">if</span> (profile) {
                <span class="hljs-keyword">return</span> profile
            }

            <span class="hljs-keyword">const</span> toCreate: Partial&lt;ShippingProfile&gt; = {
                <span class="hljs-keyword">type</span>: ShippingProfileType.CUSTOM,
                name: <span class="hljs-string">'Default Shipping Profile'</span>,
                store_id: storeId,
            }

            <span class="hljs-keyword">const</span> created = profileRepository.create(toCreate)

            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> profileRepository.save(created)
        })
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ShippingProfileService
</code></pre>
<p>Here we have wrapped our process into a <strong>transaction</strong> (<code>this.atomicPhase_</code>) , so that in the case of an error throwed when creating a user (<em>for example, an email already in use</em>), no event is sent and it will rollback changes.</p>
<h3 id="heading-create-your-first-loader">Create your first loader</h3>
<p>Before launching our server and creating ShippingOption, we're going to make sure that each store has its own default ShippingProfile.</p>
<p>To do this, we're going to make sure that, when the server starts, shipping profiles are created automatically for each new store.</p>
<p>In our case, we're going to create a file in the <code>src/loaders/create-defaults-shipping-profiles.ts</code> folder :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Logger, MedusaContainer } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> StoreRepository <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa/dist/repositories/store'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> ShippingProfileService <span class="hljs-keyword">from</span> <span class="hljs-string">'../services/shipping-profile'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">container: MedusaContainer</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
    <span class="hljs-keyword">const</span> shippingProfileService = container.resolve&lt;ShippingProfileService&gt;(<span class="hljs-string">'shippingProfileService'</span>)
    <span class="hljs-keyword">const</span> storeRepo = container.resolve&lt;<span class="hljs-keyword">typeof</span> StoreRepository&gt;(<span class="hljs-string">'storeRepository'</span>)
    <span class="hljs-keyword">const</span> logger = container.resolve&lt;Logger&gt;(<span class="hljs-string">'logger'</span>)

    <span class="hljs-keyword">const</span> allStores = <span class="hljs-keyword">await</span> storeRepo.find({
        select: [<span class="hljs-string">'id'</span>],
    })

    <span class="hljs-keyword">if</span> (!allStores.length) {
        <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">const</span> allShippingProfiles = <span class="hljs-keyword">await</span> shippingProfileService.list()

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> store <span class="hljs-keyword">of</span> allStores) {
        <span class="hljs-keyword">const</span> storeId = store.id

        <span class="hljs-comment">// For each store, we check that there is a default shipping profile</span>
        <span class="hljs-comment">// if not, we create one</span>
        <span class="hljs-keyword">if</span> (!allShippingProfiles.find(<span class="hljs-function">(<span class="hljs-params">profile</span>) =&gt;</span> profile.store_id === storeId)) {
            <span class="hljs-keyword">const</span> promise = logger.activity(<span class="hljs-string">`Creating default shipping profile for store <span class="hljs-subst">${storeId}</span>...`</span>)
            <span class="hljs-keyword">await</span> shippingProfileService.createDefaultForStore(storeId)
            logger.success(promise, <span class="hljs-string">`Created default shipping profile for store <span class="hljs-subst">${storeId}</span>!`</span>)
        }
    }
}
</code></pre>
<p>Perfect, but this will only work when we start our server, so we'll also make sure that when a store is created, a default shipping profile is created, so that nothing needs to be restarted.</p>
<h3 id="heading-update-the-storeservice">Update the <code>StoreService</code></h3>
<p>We're going to update our StoreService so that it exposes a kind of <em>enum</em> that will represent events. These events will be emitted and listened to afterwards:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/store.ts</span>

<span class="hljs-comment">// ... imports</span>

<span class="hljs-keyword">class</span> StoreService <span class="hljs-keyword">extends</span> MedusaStoreService {
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.TRANSIENT
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> loggedInUser_: User | <span class="hljs-literal">null</span>

    <span class="hljs-keyword">static</span> Events = {
        CREATED: <span class="hljs-string">'store.created'</span>, <span class="hljs-comment">// ✅ We had a static property to access easily events</span>
    }

   <span class="hljs-comment">// ... rest</span>

}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> StoreService
</code></pre>
<h3 id="heading-update-the-userservice">Update the <code>UserService</code></h3>
<p>Now we can use this event name in our UserService when creating a new store:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/user.ts</span>

<span class="hljs-comment">// ...imports</span>
<span class="hljs-keyword">class</span> UserService <span class="hljs-keyword">extends</span> MedusaUserService {
    <span class="hljs-comment">// ... rest</span>

    <span class="hljs-keyword">async</span> create(
        user: CreateUserInput,
        password: <span class="hljs-built_in">string</span>
    ): <span class="hljs-built_in">Promise</span>&lt;User&gt; {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.atomicPhase_(<span class="hljs-keyword">async</span> (m) =&gt; {
            <span class="hljs-keyword">const</span> storeRepo = m.withRepository(<span class="hljs-built_in">this</span>.storeRepository_)

            <span class="hljs-keyword">if</span> (!user.store_id) {
                <span class="hljs-keyword">let</span> newStore = storeRepo.create()
                newStore = <span class="hljs-keyword">await</span> storeRepo.save(newStore)
                user.store_id = newStore.id
            }

            <span class="hljs-keyword">const</span> savedUser = <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.create(user, password)

            <span class="hljs-built_in">this</span>.eventBus_.emit(StoreService.Events.CREATED, { id: user.store_id })

            <span class="hljs-keyword">return</span> savedUser
        })
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> UserService
</code></pre>
<h3 id="heading-create-your-first-subscriber">Create your first subscriber</h3>
<p>Once our event is ready to emit, all we need to do is create a <a target="_blank" href="https://docs.medusajs.com/development/events/subscribers">Subscriber</a> that will listen and execute code when it receives the event. In this case, we'll create a ShippingProfile each time a new store is created:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/subscribers/store-created.ts</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Logger, SubscriberArgs, SubscriberConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> ShippingProfileService <span class="hljs-keyword">from</span> <span class="hljs-string">'../services/shipping-profile'</span>
<span class="hljs-keyword">import</span> StoreService <span class="hljs-keyword">from</span> <span class="hljs-string">'../services/store'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handleStoreCreated</span>(<span class="hljs-params">{
    data,
    eventName,
    container,
    pluginOptions,
}: SubscriberArgs&lt;Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;&gt;</span>) </span>{

    <span class="hljs-keyword">const</span> shippingProfileService = container.resolve&lt;ShippingProfileService&gt;(<span class="hljs-string">"shippingProfileService"</span>)
    <span class="hljs-keyword">const</span> logger = container.resolve&lt;Logger&gt;(<span class="hljs-string">"logger"</span>)

    <span class="hljs-keyword">const</span> promise = logger.activity(<span class="hljs-string">`Creating default shipping profile for store <span class="hljs-subst">${data.id}</span>`</span>)

    <span class="hljs-keyword">await</span> shippingProfileService.createDefaultForStore(data.id).catch(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> {
        logger.error(e, <span class="hljs-string">`Error creating default shipping profile for store <span class="hljs-subst">${data.id}</span>`</span>)
        <span class="hljs-keyword">throw</span> e
    })

    logger.success(promise, <span class="hljs-string">`Default shipping profile for store <span class="hljs-subst">${data.id}</span> created`</span>)
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: SubscriberConfig = {
    event: StoreService.Events.CREATED,
    context: {
        subscriberId: <span class="hljs-string">'store-created-handler'</span>,
    },
}
</code></pre>
<p>The ShippingProfile is now inserted each time a new store is created, without having to restart our server and wait for our loader to process :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715330717676/b9842f14-9534-460a-99ba-10475a19e8e6.png" alt class="image--center mx-auto" /></p>
<p>We can now take the time to test the Admin UI with two separate accounts, for example.</p>
<p>In the screenshot below, we can see that each one sees only its own Shipping options, linked to their own Shipping Profile under the hood :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715331090044/b5f545fb-9fe1-4e97-a982-e36cbf2d6430.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-whats-next">What's Next ?</h2>
<p>In the next part, we'll look into <a target="_blank" href="https://docs.medusajs.com/modules/orders/overview"><strong>Orders</strong></a>, specifically how to ensure that each vendor can handle their own order. This will be an opportunity to use all we've learned in the previous parts, as well as to reuse the <em>Subscriber</em> notion from that current part!</p>
<h2 id="heading-github-branch">GitHub Branch</h2>
<p>You can access the complete part's code <a target="_blank" href="https://git.new/perseides-part-2-4"><strong>here</strong></a><strong>.</strong></p>
<h2 id="heading-contact">Contact</h2>
<p>You can contact me on Discord and X with the same username : <a target="_blank" href="https://twitter.com/adevinwild">@adevinwild</a></p>
]]></content:encoded></item><item><title><![CDATA[Medusa Marketplace #2.3 | Extending Shipping Profiles/Options]]></title><description><![CDATA[Hello everyone!
In the last part, we were able to customize the behavior of products on our marketplace, so vendors/users can now create products specific to their store and, of course, see only their own products.
What's the goal here ?
In this part...]]></description><link>https://blog.perseides.org/medusa-marketplace-23-extending-shipping-profilesoptions</link><guid isPermaLink="true">https://blog.perseides.org/medusa-marketplace-23-extending-shipping-profilesoptions</guid><category><![CDATA[medusa.js]]></category><category><![CDATA[medusa]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[ecommerce]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[typeorm]]></category><category><![CDATA[perseides]]></category><category><![CDATA[course]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Wed, 08 May 2024 12:04:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715169758554/3bbb57a7-0be5-40b5-a748-cd95d0bdc6b9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Hello everyone!</strong></p>
<p><a target="_blank" href="https://blog.perseides.org/marketplace-22-extending-productservice">In the last part</a>, we were able to customize the behavior of products on our marketplace, so vendors/users can now create products specific to their store and, of course, see only their own products.</p>
<h3 id="heading-whats-the-goal-here">What's the goal here ?</h3>
<p>In this part, we will extend ShippingOption to make them relevant to a store. In fact, each vendor must be able to input its own shipping options, giving the customer choices for each vendors.</p>
<p>We'll also extend the behavior of ShippingProfile to link them to a store too, because the shopping cart currently allows for multiple shipping methods, but the current implementation only allows for one per shipping profile, so we'll create one by default for each store, allowing for multiple shipping methods for a single cart.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Note that shipping profiles are supposed to be created by administrators in the current version (</strong><code>1.2x.x</code><strong>).</strong></div>
</div>

<h2 id="heading-shippingoption">ShippingOption</h2>
<h3 id="heading-extend-the-shippingoption-entity">Extend the ShippingOption entity</h3>
<p>As with our previous entities, we want a ShippingOption to be linked to a Store :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/shipping-option.ts</span>
<span class="hljs-keyword">import</span> { Column, Entity, Index, JoinColumn, ManyToOne } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { ShippingOption <span class="hljs-keyword">as</span> MedusaShippingOption } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ShippingOption <span class="hljs-keyword">extends</span> MedusaShippingOption {
    <span class="hljs-meta">@Index</span>(<span class="hljs-string">'ShippingOptionStoreId'</span>)
    <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> })
    store_id?: <span class="hljs-built_in">string</span>

    <span class="hljs-meta">@ManyToOne</span>(<span class="hljs-function">() =&gt;</span> Store, <span class="hljs-function">(<span class="hljs-params">store</span>) =&gt;</span> store.shippingOptions)
    <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">'store_id'</span>, referencedColumnName: <span class="hljs-string">'id'</span> })
    store?: Store
}
</code></pre>
<h3 id="heading-update-the-store-entity">Update the Store entity</h3>
<p>Adding a <code>ManyToOne</code> relationship above also means updating the Store model:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/store.ts</span>
<span class="hljs-keyword">import</span> { Entity, OneToMany } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Store <span class="hljs-keyword">as</span> MedusaStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'./product'</span>
<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./user'</span>
<span class="hljs-keyword">import</span> { ShippingOption } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shipping-option'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Store <span class="hljs-keyword">extends</span> MedusaStore {
    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> User, <span class="hljs-function">(<span class="hljs-params">user</span>) =&gt;</span> user.store)
    members?: User[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Product, <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> product.store)
    products?: Product[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> ShippingOption, <span class="hljs-function">(<span class="hljs-params">shippingOption</span>) =&gt;</span> shippingOption.store)
    shippingOptions?: ShippingOption[]
}
</code></pre>
<h3 id="heading-create-the-shippingoption-migration">Create the ShippingOption migration</h3>
<p>Once the entity and repository have been extended, we can now create our :</p>
<pre><code class="lang-apache"><span class="hljs-attribute">npx</span> typeorm migration:create src/migrations/add-shipping-option-store-id
</code></pre>
<p>Now that the migration file has been created, we can replace the up and down functions with these:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> up(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "shipping_option" ADD "store_id" character varying`</span>)
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`CREATE INDEX "ShippingOptionStoreId" ON "shipping_option" ("store_id")`</span>)
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> down(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`DROP INDEX "public"."ShippingOptionStoreId"`</span>)
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "shipping_option" DROP COLUMN "store_id"`</span>)
}
</code></pre>
<p>You can now build your server using the <code>yarn build</code> command and then run the <code>npx medusa migrations run</code> command, as for our previous migrations to make the changes in your database :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715169330313/d3ce9924-827e-4129-9598-cc20a0b802e9.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-shippingprofile">ShippingProfile</h2>
<h3 id="heading-extend-the-shippingprofile-entity">Extend the ShippingProfile entity</h3>
<p>Once the ShippingOption entity has been fully extended, we also need to extend the ShippingProfile entity, almost like copying and pasting what we've done above.</p>
<p>In the same way :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Column, Entity, Index, JoinColumn, ManyToOne } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { ShippingProfile <span class="hljs-keyword">as</span> MedusaShippingProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ShippingProfile <span class="hljs-keyword">extends</span> MedusaShippingProfile {
    <span class="hljs-meta">@Index</span>(<span class="hljs-string">'ShippingProfileStoreId'</span>)
    <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> })
    store_id?: <span class="hljs-built_in">string</span>

    <span class="hljs-meta">@ManyToOne</span>(<span class="hljs-function">() =&gt;</span> Store, <span class="hljs-function">(<span class="hljs-params">store</span>) =&gt;</span> store.shippingProfiles)
    <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">'store_id'</span>, referencedColumnName: <span class="hljs-string">'id'</span> })
    store?: Store
}
</code></pre>
<h3 id="heading-update-the-store-entity-1">Update the Store entity</h3>
<p>We update our Store entity to add the new <code>shippingProfiles</code> property :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/store.ts</span>
<span class="hljs-keyword">import</span> { Column, Entity, OneToMany } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Store <span class="hljs-keyword">as</span> MedusaStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./user'</span>
<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'./product'</span>
<span class="hljs-keyword">import</span> { ShippingOption } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shipping-option'</span>
<span class="hljs-keyword">import</span> { ShippingProfile } <span class="hljs-keyword">from</span> <span class="hljs-string">'./shipping-profile'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Store <span class="hljs-keyword">extends</span> MedusaStore {
    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> User, <span class="hljs-function">(<span class="hljs-params">user</span>) =&gt;</span> user.store)
    members?: User[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Product, <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> product.store)
    products?: Product[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> ShippingOption, <span class="hljs-function">(<span class="hljs-params">shippingOption</span>) =&gt;</span> shippingOption.store)
    shippingOptions?: ShippingOption[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> ShippingProfile, <span class="hljs-function">(<span class="hljs-params">shippingProfile</span>) =&gt;</span> shippingProfile.store)
    shippingProfiles?: ShippingProfile[]
}
</code></pre>
<h3 id="heading-create-the-shippingprofile-migration">Create the ShippingProfile migration</h3>
<p>We can now create the migration file for the ShippingProfile migration to apply our changes to the database :</p>
<pre><code class="lang-apache"><span class="hljs-attribute">npx</span> typeorm migration:create src/migrations/add-shipping-profile-store-id
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/migrations/&lt;TIME&gt;-add-shipping-profile-store-id.ts</span>

<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> up(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "shipping_profile" ADD "store_id" character varying`</span>)
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`CREATE INDEX "ShippingProfileStoreId" ON "shipping_profile" ("store_id")`</span>)
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> down(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`DROP INDEX "public"."ShippingProfileStoreId"`</span>)
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "shipping_profile" DROP COLUMN "store_id"`</span>)
}
<span class="hljs-comment">// ...</span>
</code></pre>
<p>By executing this new migration file, we should see the changes occurs in our database schema :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715169261250/8c6677c4-b95a-4d85-8932-413b7dfbb157.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-whats-next">What's Next ?</h2>
<p>Next, we'll look at the two services: <code>ShippingProfileService</code> and <code>ShippingOptionService</code>, and how/what we can override some of their functions. We'll use the same reasoning as the <a target="_blank" href="https://blog.perseides.org/marketplace-22-extending-productservice"><code>ProductService</code></a>, where we made sure to fetch only the products associated with a Store or tie a new product to a specific store.</p>
<h2 id="heading-github-branch">GitHub Branch</h2>
<p>You can access the complete part's code <a target="_blank" href="https://git.new/perseides-part-2-3"><strong>here</strong></a><strong>.</strong></p>
<h2 id="heading-contact">Contact</h2>
<p>You can contact me on Discord and X with the same username : <a target="_blank" href="https://twitter.com/adevinwild">@adevinwild</a></p>
]]></content:encoded></item><item><title><![CDATA[Medusa Marketplace #2.2 |  Extending ProductService]]></title><description><![CDATA[Hello everyone!
Now that our vendors have access to their store, I can tell they are getting bored. Indeed, without any things to sell, our back office is useless, therefore we're spending this time expanding the product concept and adding a decent b...]]></description><link>https://blog.perseides.org/marketplace-22-extending-productservice</link><guid isPermaLink="true">https://blog.perseides.org/marketplace-22-extending-productservice</guid><category><![CDATA[medusa]]></category><category><![CDATA[medusa.js]]></category><category><![CDATA[marketplace]]></category><category><![CDATA[perseides]]></category><category><![CDATA[course]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Tue, 07 May 2024 09:31:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715162336876/99eee263-accd-4dc9-853f-9a0e7b6ba77c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Hello everyone!</strong></p>
<p>Now that our vendors have access to their store, I can tell they are getting bored. Indeed, without any things to sell, our back office is useless, therefore we're spending this time expanding the product concept and adding a decent bit of customization.</p>
<h2 id="heading-what-is-the-goal-here">What is the goal here ?</h2>
<p>The benefit of this section of the tutorial is that it allows you to apply everything you've learned in prior sections.</p>
<p>First, we will:</p>
<ul>
<li><p><strong>Create a new file in the</strong><code>models</code><strong>folder, that will extend the core entity with new properties</strong></p>
</li>
<li><p><strong>Create a new migration using the TypeORM CLI</strong></p>
</li>
<li><p><strong>Create a new file in the</strong><code>services</code><strong>folder that will initially only include our basic changes (</strong><code>LIFE_TIME</code><strong>,</strong><code>this.loggedInUser_</code><strong>...)</strong></p>
</li>
</ul>
<p>Once these steps are completed, we'll get down to the essential of adding these features :</p>
<ul>
<li><p><strong>Create a product related to a vendor</strong></p>
</li>
<li><p><strong>List the products of a vendor</strong></p>
</li>
</ul>
<h2 id="heading-extend-the-product-entity">Extend the Product entity</h2>
<p>Here is where we define the new schema for the product table. In fact, we want to associate a product with a store, therefore we use the same reasoning as the User table we extended earlier :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/product.ts</span>
<span class="hljs-keyword">import</span> { Column, Entity, Index, JoinColumn, ManyToOne } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Product <span class="hljs-keyword">as</span> MedusaProduct } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'./store'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Product <span class="hljs-keyword">extends</span> MedusaProduct {
    <span class="hljs-meta">@Index</span>(<span class="hljs-string">'ProductStoreId'</span>)
    <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> })
    store_id?: <span class="hljs-built_in">string</span>

    <span class="hljs-meta">@ManyToOne</span>(<span class="hljs-function">() =&gt;</span> Store, <span class="hljs-function">(<span class="hljs-params">store</span>) =&gt;</span> store.products)
    <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">'store_id'</span>, referencedColumnName: <span class="hljs-string">'id'</span> })
    store?: Store
}
</code></pre>
<p>A relationship with the <code>Store</code> implies an update of the previously extended <code>Store</code> model:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/store.ts</span>
<span class="hljs-keyword">import</span> { Entity, OneToMany } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>

<span class="hljs-keyword">import</span> { Store <span class="hljs-keyword">as</span> MedusaStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>

<span class="hljs-keyword">import</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'./product'</span>
<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./user'</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Store <span class="hljs-keyword">extends</span> MedusaStore {
    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> User, <span class="hljs-function">(<span class="hljs-params">user</span>) =&gt;</span> user.store)
    members?: User[]

    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> Product, <span class="hljs-function">(<span class="hljs-params">product</span>) =&gt;</span> product.store)
    products?: Product[]
}
</code></pre>
<h3 id="heading-create-a-migration">Create a Migration</h3>
<p>Once the preceding parts are completed, we may create a migration to notify the database of certain changes; the command is as follows :</p>
<pre><code class="lang-apache"><span class="hljs-attribute">npx</span> typeorm migration:create src/migrations/add-product-store-id
</code></pre>
<p>Once the migration file has been created, we can update the <code>up</code> and <code>down</code> functions like this :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/migrations/...-add-product-store-id.ts</span>

<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> up(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "product" ADD "store_id" character varying`</span>)
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`CREATE INDEX "ProductStoreId" ON "product" ("store_id")`</span>)
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> down(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`DROP INDEX "public"."ProductStoreId"`</span>)
    <span class="hljs-keyword">await</span> queryRunner.query(<span class="hljs-string">`ALTER TABLE "product" DROP COLUMN "store_id"`</span>)
}
<span class="hljs-comment">// ...</span>
</code></pre>
<p>Perfect, <a target="_blank" href="https://blog.perseides.org/marketplace-lets-follow-the-recipe?t=1715156441170#heading-how-to-apply-migrations">now we can run build our server and run the migrations</a>, after that, the database we should see the new column in the <code>product</code> table :</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715069489835/1f99b342-22ce-4810-96b5-6752dfc3d737.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-adding-new-features">Adding new features</h2>
<h3 id="heading-create-a-product-for-a-store">Create a Product for a Store</h3>
<p>To create a Product tied to a Store, we'll start by extending the <code>ProductService</code> and overriding the <code>create</code> function to force the logged-in user's <code>store_id</code> into the product before inserting it. However, you will see type problems with the input type expected on the method, therefore we will need to expand the type to add our new <code>store_id</code> property :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/product.ts</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { CreateProductInput <span class="hljs-keyword">as</span> MedusaCreateProductInput } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa/dist/types/product'</span>
<span class="hljs-keyword">import</span> { ProductService <span class="hljs-keyword">as</span> MedusaProductService } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">'awilix'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/user'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/product'</span>

<span class="hljs-comment">// We override the type definition so it will not throw TS errors in the `create` method</span>
<span class="hljs-keyword">type</span> CreateProductInput = {
    store_id?: <span class="hljs-built_in">string</span>
} &amp; MedusaCreateProductInput

<span class="hljs-keyword">class</span> ProductService <span class="hljs-keyword">extends</span> MedusaProductService {
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.TRANSIENT
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> loggedInUser_: User | <span class="hljs-literal">null</span>

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">container</span>) {
        <span class="hljs-comment">// @ts-ignore</span>
        <span class="hljs-built_in">super</span>(...arguments)

        <span class="hljs-keyword">try</span> {
            <span class="hljs-built_in">this</span>.loggedInUser_ = container.loggedInUser
        } <span class="hljs-keyword">catch</span> (e) {
            <span class="hljs-comment">// avoid errors when backend first runs</span>
        }
    }

    <span class="hljs-keyword">async</span> create(productObject: CreateProductInput): <span class="hljs-built_in">Promise</span>&lt;Product&gt; {
        <span class="hljs-keyword">if</span> (!productObject.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            productObject.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.create(productObject)
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> ProductService
</code></pre>
<p>Okay, at our level here, we can be sure that a User who is logged in AND has a <code>µ store_id</code> must be able to create a product for its store.</p>
<p>On the other hand, when a User wants to retrieve products, there are currently no constraints preventing him from seeing ONLY its store's products, let's implement this feature.</p>
<h3 id="heading-list-products-for-a-store">List Products for a Store</h3>
<p><a target="_blank" href="https://github.com/medusajs/medusa/blob/v1.20.4/packages/medusa/src/services/product.ts#L156"><code>ProductService.listAndCount</code></a> is the function that retrieves products used in the back-office, therefore we'll alter it to include the logged-in user's <code>store_id</code> to retrieve only his products. However, the selector of this function does not know the <code>store_id</code> property at all, so we will expand the selector to make it aware of that new property :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ... rest</span>
<span class="hljs-keyword">import</span> { ProductSelector <span class="hljs-keyword">as</span> MedusaProductSelector } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa/dist/types/product'</span>

<span class="hljs-keyword">type</span> ProductSelector = {
    store_id?: <span class="hljs-built_in">string</span>
} &amp; MedusaProductSelector

<span class="hljs-keyword">class</span> ProductService <span class="hljs-keyword">extends</span> MedusaProductService {
    <span class="hljs-comment">// ... rest</span>

    <span class="hljs-keyword">async</span> listAndCount(selector: ProductSelector, config?: FindProductConfig): <span class="hljs-built_in">Promise</span>&lt;[Product[], <span class="hljs-built_in">number</span>]&gt; {
        <span class="hljs-keyword">if</span> (!selector.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            selector.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id
        }

        config.select?.push(<span class="hljs-string">'store_id'</span>)

        config.relations?.push(<span class="hljs-string">'store'</span>)

        <span class="hljs-keyword">const</span> products = <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.listAndCount(selector, config)
        <span class="hljs-keyword">return</span> products
    }
}
</code></pre>
<p>You can now try it on the Admin UI or directly with the API, and you should get the behavior expected.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>You can also override others method like </strong><a target="_blank" href="https://github.com/medusajs/medusa/blob/v1.20.4/packages/medusa/src/services/product.ts#L242"><code>ProductService.retrieve</code></a><strong>, </strong><a target="_blank" href="https://github.com/medusajs/medusa/blob/v1.20.4/packages/medusa/src/services/product.ts#L620"><code>ProductService.update</code></a><strong> and more, depending on your needs</strong></div>
</div>

<h2 id="heading-whats-next">What's Next ?</h2>
<p>We now have the foundation; vendors can access the admin UI and create/list products associated with their store; in the next phase, we will extend entities to <strong>allow each store to create its own shipping options.</strong></p>
<h2 id="heading-common-issues">Common Issues</h2>
<p><strong>A product handle must be unique</strong></p>
<p>The <code>product.handle</code> might create an issue in the long term when creating a product for a store, <strong>as it's supposed to be unique</strong>. You can for example either add a prefix or suffix containing the <code>store_id</code> to avoid any problems :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/product.ts</span>
<span class="hljs-keyword">class</span> ProductService <span class="hljs-keyword">extends</span> MedusaProductService {
    <span class="hljs-comment">// ... rest</span>
    <span class="hljs-keyword">async</span> create(productObject: CreateProductInput): <span class="hljs-built_in">Promise</span>&lt;Product&gt; {
        <span class="hljs-keyword">if</span> (!productObject.store_id &amp;&amp; <span class="hljs-built_in">this</span>.loggedInUser_?.store_id) {
            productObject.store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id

            <span class="hljs-comment">// This will generate a handle for the product based on the title and store_id</span>
            <span class="hljs-comment">// e.g. "sunglasses-01HXVYMJF9DW..."</span>
            <span class="hljs-keyword">const</span> title = productObject.title.normalize(<span class="hljs-string">'NFD'</span>).replace(<span class="hljs-regexp">/[\u0300-\u036f]/g</span>, <span class="hljs-string">''</span>).replace(<span class="hljs-regexp">/\s+/g</span>, <span class="hljs-string">'-'</span>).toLowerCase()
            <span class="hljs-keyword">const</span> store_id = <span class="hljs-built_in">this</span>.loggedInUser_.store_id.replace(<span class="hljs-string">"store_"</span>, <span class="hljs-string">""</span>)

            productObject.handle = <span class="hljs-string">`<span class="hljs-subst">${title}</span>-<span class="hljs-subst">${store_id}</span>`</span>
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.create(productObject)
    }
}
</code></pre>
<p><strong>I want to retrieve products from a specific store only from my storefront</strong></p>
<p>For that you'll need to create a <a target="_blank" href="https://docs.medusajs.com/development/loaders/overview">loader</a>, that will extend the default <code>/store/products</code> relations and allowed fields.</p>
<p>First you'll need to create a new loader in the <code>/src/loaders/</code> directory :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/loaders/extend-store-products.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
    <span class="hljs-keyword">const</span> <span class="hljs-keyword">module</span> = await import('@medusajs/medusa/dist/api/routes/store/products/index')

    Object.assign(<span class="hljs-keyword">module</span>, {
        ...module,
        defaultStoreProductsRelations: [...module.defaultStoreProductsRelations, <span class="hljs-string">'store'</span>],
        defaultStoreProductsFields: [...module.defaultStoreProductsFields, <span class="hljs-string">'store_id'</span>],
        allowedStoreProductsRelations: [...module.allowedStoreProductsRelations, <span class="hljs-string">'store'</span>],
        allowedStoreProductsFields: [...module.allowedStoreProductsFields, <span class="hljs-string">'store_id'</span>],
    })
}
</code></pre>
<p>Then we'll extend the validators of the <code>/store/products</code> API route, the goal here is to be able to add a query parameter <code>/store/products?store_id=&lt;STORE_ID&gt;</code> , and the default Medusa validator for that API is not aware of the new property we wants to add.</p>
<p>Next step, is to create a file in the <code>/src/api/</code> folder, named specifically <code>index.ts</code> .</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/api/index.ts</span>
<span class="hljs-keyword">import</span> { registerOverriddenValidators } <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa"</span>

<span class="hljs-comment">// Here we are importing the original Medusa validator as an alias :</span>
<span class="hljs-keyword">import</span> {
   StoreGetProductsParams <span class="hljs-keyword">as</span> MedusaStoreGetProductsParams,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa/dist/api/routes/store/products/list-products"</span>
<span class="hljs-keyword">import</span> { IsString, IsOptional } <span class="hljs-keyword">from</span> <span class="hljs-string">"class-validator"</span>

<span class="hljs-comment">// Here we add the new allowed property `store_id` :</span>
<span class="hljs-keyword">class</span> StoreGetProductsParams <span class="hljs-keyword">extends</span> MedusaStoreGetProductsParams {
   <span class="hljs-meta">@IsString</span>()
   <span class="hljs-meta">@IsOptional</span>() <span class="hljs-comment">// Optional of course</span>
   store_id?: <span class="hljs-built_in">string</span>
}

<span class="hljs-comment">// The following function will replace the original validator by the new one.</span>
registerOverriddenValidators(StoreGetProductsParams)
</code></pre>
<p>Now you can successfully retrieve all products from a specific store like this.</p>
<pre><code class="lang-typescript">curl http:<span class="hljs-comment">//localhost:9000/store/products?store_id=&lt;YOUR_STORE_ID&gt;</span>
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The URL is just an example, replace by your own backend URL.</div>
</div>

<p><strong>I have TypeScript errors !</strong></p>
<p>In the same way that we have notified the database of new changes thanks to <a target="_blank" href="https://docs.medusajs.com/development/entities/migrations/create">migrations</a>, we will have to do the same with our packages, which are not aware of new properties.</p>
<p>Remember to declare your types according to your needs. In our case, we only need the following types :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/index.d.ts</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Product } <span class="hljs-keyword">from</span> <span class="hljs-string">'./models/product'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">'./models/store'</span>

<span class="hljs-keyword">declare</span> <span class="hljs-keyword">module</span> '@medusajs/medusa/dist/models/product' {
    <span class="hljs-keyword">interface</span> Product {
        store_id?: <span class="hljs-built_in">string</span>
        store?: Store
    }
}
</code></pre>
<h2 id="heading-github-branch">GitHub Branch</h2>
<p>You can access the complete part's code <a target="_blank" href="https://git.new/perseides-part-2-2"><strong>here</strong></a><strong>.</strong></p>
<h2 id="heading-contact">Contact</h2>
<p>You can contact me on Discord and X with the same username : <a target="_blank" href="https://twitter.com/adevinwild">@adevinwild</a></p>
]]></content:encoded></item><item><title><![CDATA[Medusa Marketplace #2.1 | Extending StoreService]]></title><description><![CDATA[Hello everyone!
In this part of our Medusa.js marketplace journey, we'll continue building on the groundwork we established in the previous sections. This time, our focus will be on extending the StoreService and ProductService to ensure a seamless u...]]></description><link>https://blog.perseides.org/marketplace-21-extending-storeservice</link><guid isPermaLink="true">https://blog.perseides.org/marketplace-21-extending-storeservice</guid><category><![CDATA[perseides]]></category><category><![CDATA[medusa.js]]></category><category><![CDATA[medusa]]></category><category><![CDATA[marketplace]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[course]]></category><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Tue, 07 May 2024 09:30:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715162324056/d59c595b-3147-4849-9f5d-0fc229b29f4c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Hello everyone!</strong></p>
<p>In this part of our Medusa.js marketplace journey, we'll continue building on the groundwork we established in the previous sections. This time, our focus will be on extending the <code>StoreService</code> and <code>ProductService</code> to ensure a seamless user experience.</p>
<h2 id="heading-what-is-the-goal-here">What is the goal here ?</h2>
<p>The core objective here is to guarantee that when a user logs into the admin ui, they can seamlessly access their own store based on their <code>store_id</code>. This level of personalization is crucial for providing a tailored experience within our marketplace. As you've noted, the approach involves extending the existing services to achieve this functionality. Let's dive into the details.</p>
<h2 id="heading-extend-the-store-service">Extend the Store Service</h2>
<p>We'll create a new file in the <code>services</code> folder, named <code>store.ts</code>, where we'll extend the <code>StoreService</code> to override the <code>retrieve</code> function.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/store.ts</span>
<span class="hljs-keyword">import</span> { <span class="hljs-keyword">type</span> FindConfig, StoreService <span class="hljs-keyword">as</span> MedusaStoreService, buildQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa"</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">'awilix'</span>

<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">"../models/user"</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../models/store"</span>

<span class="hljs-keyword">class</span> StoreService <span class="hljs-keyword">extends</span> MedusaStoreService {
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.TRANSIENT
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> loggedInUser_: User | <span class="hljs-literal">null</span>

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">container, options</span>) {
        <span class="hljs-comment">// @ts-ignore</span>
        <span class="hljs-built_in">super</span>(...arguments)

        <span class="hljs-keyword">try</span> {
            <span class="hljs-built_in">this</span>.loggedInUser_ = container.loggedInUser
        } <span class="hljs-keyword">catch</span> (e) {
            <span class="hljs-comment">// avoid errors when backend first runs</span>
        }
    }

     <span class="hljs-keyword">async</span> retrieve(config?: FindConfig&lt;Store&gt;): <span class="hljs-built_in">Promise</span>&lt;Store&gt; {
            <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.loggedInUser_ || !<span class="hljs-built_in">this</span>.loggedInUser_.store_id) {
                <span class="hljs-comment">// In case there is no loggedInUser or no store_id, </span>
                <span class="hljs-comment">// we just use the original function </span>
                <span class="hljs-comment">// that retrieves the default store</span>
                <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.retrieve(config)
            }

            <span class="hljs-keyword">const</span> storeRepo = <span class="hljs-built_in">this</span>.activeManager_.withRepository(<span class="hljs-built_in">this</span>.storeRepository_)

            <span class="hljs-keyword">const</span> query = buildQuery&lt;Partial&lt;Store&gt;, Store&gt;(
                {
                    id: <span class="hljs-built_in">this</span>.loggedInUser_.store_id,
                },
                {
                    ...config,
                    relations: [...(config?.relations || []), <span class="hljs-string">'members'</span>],
                },
            )

            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> storeRepo.findOne(query)
        }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> StoreService
</code></pre>
<p>In this extended <code>StoreService</code>, we override the <code>retrieve</code> method to check if the logged-in user is defined and have a <code>store_id</code> associated with their account. If so, we use that <code>store_id</code> to retrieve that specific store information. Otherwise, we fall back to the default behavior of the <code>StoreService</code> (<em>which will just use the default store created by the Medusa core loaders</em>)</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>I recommend that you adjust the store names you currently have in your database</strong> so that when you log in with a vendor account, you see the correct shop name. In reality, the default store name is Medusa Store, thus it might be easier for you to have alternative names so you don't struggle with it.</div>
</div>

<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715066806814/a44944c9-f44a-466c-aa8b-5528330a3bba.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-whats-next">What's Next ?</h2>
<p>Well done, now that the right store is displayed, we will extend the products in the next part!</p>
<h2 id="heading-github-branch">GitHub Branch</h2>
<p>You can access the complete part's code <a target="_blank" href="https://git.new/perseides-part-2-1"><strong>here</strong></a><strong>.</strong></p>
<h2 id="heading-contact">Contact</h2>
<p>You can contact me on Discord and X with the same username : <a target="_blank" href="https://twitter.com/adevinwild">@adevinwild</a></p>
]]></content:encoded></item><item><title><![CDATA[Medusa Marketplace #1 | Let's follow THE recipe]]></title><description><![CDATA[Hello Everyone,My name is Adil (a.k.aadevinwild), and I'm passionate about Medusa.js.
Last year, I embarked on my first (client) project using Medusa.js to build a marketplace. Since then, Medusa has undergone significant improvements, and I believe ...]]></description><link>https://blog.perseides.org/marketplace-lets-follow-the-recipe</link><guid isPermaLink="true">https://blog.perseides.org/marketplace-lets-follow-the-recipe</guid><category><![CDATA[medusa.js]]></category><category><![CDATA[medusa]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[typeorm]]></category><category><![CDATA[marketplace]]></category><category><![CDATA[course]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Series]]></category><dc:creator><![CDATA[Adil]]></dc:creator><pubDate>Tue, 07 May 2024 09:16:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715162308239/76b7e4f3-c02c-495f-8851-c1116340dfdb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Hello Everyone,</strong><br />My name is Adil (<em>a.k.a</em><a target="_blank" href="https://x.com/adevinwild"><em>adevinwild</em></a>), and I'm passionate about Medusa.js.</p>
<p><strong>Last year, I embarked on my first (client) project using Medusa.js</strong> to build a marketplace. Since then, Medusa has undergone significant improvements, and I believe it's beneficial to share insights on constructing a marketplace with the latest versions.</p>
<h3 id="heading-a-marketplace-is">A marketplace is...</h3>
<p><strong>A marketplace, conceptually, is a platform where multiple vendors can sell various products to numerous customers.</strong> One key aspect I've learned is that typically, the <strong>marketplace retains funds until certain conditions are met</strong> (<em>e.g an order has been delivered to the customer</em>), <strong>after which payouts are made to vendors</strong>, including <em>fees</em>.</p>
<p>In this blog series, we'll delve into building a <strong>specific type of marketplace—a clothing marketplace where anyone can register and start selling their products.</strong></p>
<p>While we won't cover every detail, the aim is to equip you with the knowledge to customize Medusa.js for creating a simple yet functional marketplace.<br />You'll likely need to manage many aspects independently, but this guide will prepare you well.</p>
<p>It's important to note that not every marketplace will be identical, and the provided examples might not exactly match the marketplace you envision.</p>
<p>Let's start by following the recipe detailed in the Medusa documentation. The documentation covers various concepts and sometimes includes actual code implementations.</p>
<p>This will be a heavy first part, as it will condense certain points from the official documentation, but I promise that the rest of the parts will be lighter in terms of text.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">📣</div>
<div data-node-type="callout-text"><strong>Before we proceed, I'll assume you've already </strong><a target="_blank" href="https://docs.medusajs.com/create-medusa-app"><strong>set up your Medusa app</strong></a><strong>.</strong></div>
</div>

<h2 id="heading-before-we-start">Before we start...</h2>
<p>As we progress through this series, we will be enhancing and altering some of Medusa's fundamental logic and functionality.</p>
<p>However, the ability to use the <a target="_blank" href="https://docs.medusajs.com/modules/sales-channels/overview">sales channels feature flag</a> may be impossible upon the design approach taken.</p>
<p>The current design of the Cart system, particularly its integration with sales channels, poses a challenge. It is structured in a way that precludes the possibility of associating multiple sales channels with a single Cart.</p>
<p>If you wish to leverage the sales channels feature, you have the flexibility to modify the Cart system or develop a similar mechanism to fully capitalize on its capabilities.</p>
<p>You'll learn how to disable the <code>sales_channels</code> feature flag by clicking <a target="_blank" href="https://docs.medusajs.com/development/feature-flags/toggle">here</a>.</p>
<h2 id="heading-following-the-recipe">Following the recipe</h2>
<p>The first part of our journey involves extending entities. The documentation guides us on how to begin this process, starting with the concept of a <code>User</code> linked to a <code>Store</code> (where the <code>User</code> acts as a vendor, it's distinct from a <code>Customer</code>).</p>
<h3 id="heading-extending-the-user-entity">Extending the User entity</h3>
<p>Here’s how we can start extending the <code>User</code> entity:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/user.ts</span>
<span class="hljs-keyword">import</span> { 
  Column,
  Entity,
  Index,
  JoinColumn,
  ManyToOne,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"typeorm"</span>
<span class="hljs-keyword">import</span> {
  User <span class="hljs-keyword">as</span> MedusaUser,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa"</span>
<span class="hljs-keyword">import</span> { Store } <span class="hljs-keyword">from</span> <span class="hljs-string">"./store"</span>

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> User <span class="hljs-keyword">extends</span> MedusaUser {
  <span class="hljs-meta">@Index</span>(<span class="hljs-string">"UserStoreId"</span>)
  <span class="hljs-meta">@Column</span>({ nullable: <span class="hljs-literal">true</span> })
  store_id?: <span class="hljs-built_in">string</span>

  <span class="hljs-meta">@ManyToOne</span>(<span class="hljs-function">() =&gt;</span> Store, <span class="hljs-function">(<span class="hljs-params">store</span>) =&gt;</span> store.members)
  <span class="hljs-meta">@JoinColumn</span>({ name: <span class="hljs-string">"store_id"</span>, referencedColumnName: <span class="hljs-string">"id"</span> })
  store?: Store
}
</code></pre>
<h3 id="heading-extending-the-store-entity">Extending the Store entity</h3>
<p>Next, we extend the <code>Store</code> entity to include a new property representing our <code>OneToMany</code> relationship, which is necessary to avoid type errors in the previously extended <code>User</code> model :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/models/store.ts</span>
<span class="hljs-keyword">import</span> { Entity, OneToMany } <span class="hljs-keyword">from</span> <span class="hljs-string">'typeorm'</span>;
<span class="hljs-keyword">import</span> { Store <span class="hljs-keyword">as</span> MedusaStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>;
<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./user'</span>;

<span class="hljs-meta">@Entity</span>()
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> Store <span class="hljs-keyword">extends</span> MedusaStore {
    <span class="hljs-meta">@OneToMany</span>(<span class="hljs-function">() =&gt;</span> User, <span class="hljs-function">(<span class="hljs-params">user</span>) =&gt;</span> user.store)
    members?: User[];
}
</code></pre>
<h3 id="heading-our-first-migration">Our First Migration</h3>
<p>After extending the <code>User</code> and <code>Store</code> entities, we proceed with a database migration to reflect these changes in the schema. We continue following the recipe by executing the following command in our terminal:</p>
<pre><code class="lang-apache"><span class="hljs-attribute">npx</span> typeorm migration:create src/migrations/add-user-store-id
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠</div>
<div data-node-type="callout-text"><mark>It's crucial to manually write SQL queries when adding new columns to an existing (core) entity to avoid errors. Detailed instructions are available in the callout section of the </mark><a target="_blank" class="break-word hover:text-super hover:decoration-super dark:hover:text-superDark dark:hover:decoration-superDark underline decoration-from-font underline-offset-1 transition-all duration-300" href="https://docs.medusajs.com/development/entities/migrations/create"><mark>Medusa documentation on </mark></a><a target="_blank" href="http://migrations.Here"><mark>migrations.</mark></a></div>
</div>

<p>Here's how we replace the <code>up</code> and <code>down</code> functions in our new migration with the ones provided in the docs :</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> up(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">await</span> queryRunner.query(
      <span class="hljs-string">`ALTER TABLE "user" ADD "store_id" character varying`</span>
    );
    <span class="hljs-keyword">await</span> queryRunner.query(
      <span class="hljs-string">`CREATE INDEX "UserStoreId" ON "user" ("store_id")`</span>
    );
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> down(queryRunner: QueryRunner): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">await</span> queryRunner.query(
      <span class="hljs-string">`DROP INDEX "public"."UserStoreId"`</span>
    );
    <span class="hljs-keyword">await</span> queryRunner.query(
      <span class="hljs-string">`ALTER TABLE "user" DROP COLUMN "store_id"`</span>
    );
}
</code></pre>
<h3 id="heading-how-to-apply-migrations">How to apply migrations ?</h3>
<p>To apply these migrations, we build our server and run the CLI command:</p>
<pre><code class="lang-apache"><span class="hljs-attribute">yarn</span> build
<span class="hljs-attribute">npx</span> medusa migrations run
</code></pre>
<p>If we check our database now, we should see a new <code>store_id</code> column in the <code>user</code> table!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715000867719/0d119fe9-13f4-4a20-90ca-4e5ec4cd6adb.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-our-first-middleware"><strong>Our First Middleware</strong></h3>
<p>We have just set up the foundation for our marketplace by extending the User and Store entities and creating the necessary migrations. Now, we'll dive deeper into customizing the data management functionalities using middleware and service extensions.</p>
<h3 id="heading-registering-a-logged-in-user-middleware"><strong>Registering a Logged-in User / Middleware</strong></h3>
<p>Medusa.js provides a way to create middleware that can be applied to specific routes. In the context of our marketplace, we want to ensure that the logged-in user's information is available throughout our application, especially when retrieving data. Let's create a new middleware that will register the logged-in user in the request scope:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/api/middlewares.ts</span>
<span class="hljs-keyword">import</span> { 
  authenticate,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa"</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { 
  MedusaNextFunction, 
  MedusaRequest, 
  MedusaResponse,
  MiddlewaresConfig, 
  User, 
  UserService,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa"</span>

<span class="hljs-keyword">const</span> registerLoggedInUser = <span class="hljs-keyword">async</span> (
  req: MedusaRequest, 
  res: MedusaResponse, 
  next: MedusaNextFunction
) =&gt; {
  <span class="hljs-keyword">let</span> loggedInUser: User | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>

  <span class="hljs-keyword">if</span> (req.user &amp;&amp; req.user.userId) {
    <span class="hljs-keyword">const</span> userService = 
      req.scope.resolve(<span class="hljs-string">"userService"</span>) <span class="hljs-keyword">as</span> UserService
    loggedInUser = <span class="hljs-keyword">await</span> userService.retrieve(req.user.userId)
  }

  req.scope.register({
    loggedInUser: {
      resolve: <span class="hljs-function">() =&gt;</span> loggedInUser,
     },
   })

  next()
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: MiddlewaresConfig = {
  routes: [
    {
      matcher: <span class="hljs-regexp">/^\/admin\/(?!auth|analytics-config|users\/reset-password|users\/password-token|invites\/accept).*/</span>,
      middlewares: [authenticate(), registerLoggedInUser],
    },
  ],
}
</code></pre>
<p>In this middleware, we first check if there is a logged-in user in the request. If so, we retrieve the user's information using the <code>userService</code>.</p>
<p>We then register the <code>loggedInUser</code> value in the request scope, which can be accessed by other services and components.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>The middleware is applied to all </strong><code>/admin</code><strong> routes, except for a few specific routes that don't require authentication (<em>This is why the long regex</em>)</strong></div>
</div>

<h3 id="heading-extending-the-user-service">Extending the User Service</h3>
<p>Next, let's extend the <code>UserService</code> to automatically create a new store when a new user is registered. This will ensure that every user has a store associated with their account, which is a key requirement for <em>our</em> marketplace :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/services/user.ts</span>
<span class="hljs-keyword">import</span> { Lifetime } <span class="hljs-keyword">from</span> <span class="hljs-string">"awilix"</span>
<span class="hljs-keyword">import</span> {
    UserService <span class="hljs-keyword">as</span> MedusaUserService,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa"</span>
<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">"../models/user"</span>
<span class="hljs-keyword">import</span> {
    CreateUserInput <span class="hljs-keyword">as</span> MedusaCreateUserInput,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa/dist/types/user"</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> StoreRepository <span class="hljs-keyword">from</span> <span class="hljs-string">"@medusajs/medusa/dist/repositories/store"</span>

<span class="hljs-keyword">type</span> CreateUserInput = {
    store_id?: <span class="hljs-built_in">string</span>
} &amp; MedusaCreateUserInput

<span class="hljs-keyword">class</span> UserService <span class="hljs-keyword">extends</span> MedusaUserService {
    <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.SCOPED
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> loggedInUser_: User | <span class="hljs-literal">null</span>
    <span class="hljs-keyword">protected</span> <span class="hljs-keyword">readonly</span> storeRepository_: <span class="hljs-keyword">typeof</span> StoreRepository

    <span class="hljs-keyword">constructor</span>(<span class="hljs-params">container, options</span>) {
        <span class="hljs-comment">// @ts-ignore</span>
        <span class="hljs-built_in">super</span>(...arguments)

        <span class="hljs-built_in">this</span>.storeRepository_ = container.storeRepository

        <span class="hljs-keyword">try</span> {
            <span class="hljs-built_in">this</span>.loggedInUser_ = container.loggedInUser
        } <span class="hljs-keyword">catch</span> (e) {
            <span class="hljs-comment">// avoid errors when backend first runs</span>
        }
    }

    <span class="hljs-keyword">async</span> create(
        user: CreateUserInput,
        password: <span class="hljs-built_in">string</span>
    ): <span class="hljs-built_in">Promise</span>&lt;User&gt; {
        <span class="hljs-keyword">if</span> (!user.store_id) {
            <span class="hljs-keyword">const</span> storeRepo = <span class="hljs-built_in">this</span>.manager_.withRepository(
                <span class="hljs-built_in">this</span>.storeRepository_
            )
            <span class="hljs-keyword">let</span> newStore = storeRepo.create()
            newStore = <span class="hljs-keyword">await</span> storeRepo.save(newStore)
            user.store_id = newStore.id
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-built_in">super</span>.create(user, password)
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> UserService
</code></pre>
<p>In this extended <code>UserService</code>, we override the <code>create</code> method to check if the user has a <code>store_id</code> associated with their account. If not, we create a new store and associate it with the user's account before creating the new user.</p>
<p>Let's run our server and try to create a new <code>User</code>, we can use <a target="_blank" href="https://www.postman.com/">Postman</a> or <a target="_blank" href="https://httpie.io/">HTTPie</a> to make a <code>POST</code> request to <code>/admin/users</code> when logged-in.</p>
<p>Let's try with a simple payload like this one :</p>
<pre><code class="lang-json"><span class="hljs-comment">// [POST] /admin/users</span>
{
    <span class="hljs-attr">"email"</span>:<span class="hljs-string">"john@doe.com"</span>,
    <span class="hljs-attr">"password"</span>:<span class="hljs-string">"1234"</span>
}
</code></pre>
<p>You should get a server's response like this one :</p>
<pre><code class="lang-json">{
   <span class="hljs-attr">"user"</span>: {
      <span class="hljs-attr">"email"</span>:<span class="hljs-string">"john@doe.com"</span>,
      <span class="hljs-attr">"store_id"</span>:<span class="hljs-string">"store_01HX6NFWW7R7306VHQ1PSP8YN7"</span>, 
      <span class="hljs-comment">// ✅ ☝️ Just what we wanted!</span>
      <span class="hljs-attr">"id"</span>:<span class="hljs-string">"usr_01HX6NFX8G78KHB6KD3MDS69VH"</span>,
      <span class="hljs-attr">"role"</span>:<span class="hljs-string">"member"</span>,
      <span class="hljs-attr">"first_name"</span>:<span class="hljs-literal">null</span>,
      <span class="hljs-attr">"last_name"</span>:<span class="hljs-literal">null</span>,
      <span class="hljs-attr">"api_token"</span>:<span class="hljs-literal">null</span>,
      <span class="hljs-attr">"metadata"</span>:<span class="hljs-literal">null</span>,
      <span class="hljs-attr">"created_at"</span>:<span class="hljs-string">"2024-05-06T09:59:19.1182"</span>,
      <span class="hljs-attr">"updated_at"</span>:<span class="hljs-string">"2024-05-06T09:59:19.118Z"</span>,
      <span class="hljs-attr">"deleted_at"</span>:<span class="hljs-literal">null</span>
   }
}
</code></pre>
<p>Now, whenever we create a new <code>User</code> a new <code>Store</code> is associated to it !</p>
<h2 id="heading-whats-next">What's Next ?</h2>
<p>And this is where this first part of the series ends, as the recipe gives us a few more concepts, such as associating a <code>store_id</code> with the creation of a product etc., before moving on to the events/subscribers.</p>
<p>We're not going to tackle events just yet, but rather continue to focus on service management and how to extend them so as to have everything nicely tied up to a vendor.</p>
<h2 id="heading-common-issues">Common Issues</h2>
<p><strong>I have CORS errors since I've added middlewares</strong></p>
<p>If you have any CORS errors when accessing the Admin UI, this might solves your issue</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Do not forget to add the environment variable <code>ADMIN_CORS</code> in your <code>.env</code> file</div>
</div>

<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { authenticate, <span class="hljs-keyword">type</span> MiddlewaresConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'@medusajs/medusa'</span>
<span class="hljs-keyword">import</span> { parseCorsOrigins } <span class="hljs-keyword">from</span> <span class="hljs-string">'medusa-core-utils'</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cors <span class="hljs-keyword">from</span> <span class="hljs-string">'cors'</span> <span class="hljs-comment">// ⚠️ Make sure you import it like this</span>

<span class="hljs-keyword">import</span> registerLoggedInUser <span class="hljs-keyword">from</span> <span class="hljs-string">'./middlewares/register-logged-in-user'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> config: MiddlewaresConfig = {
    routes: [
        {
            matcher: <span class="hljs-regexp">/^\/admin\/(?!auth|analytics-config|users\/reset-password|users\/password-token|invites\/accept).*/</span>,
            middlewares: [
                cors.default({ credentials: <span class="hljs-literal">true</span>, origin: parseCorsOrigins(process.env.ADMIN_CORS ?? <span class="hljs-string">''</span>) }),
                authenticate(),
                registerLoggedInUser,
            ],
        },
    ],
}
</code></pre>
<p><strong>My</strong><code>loggedInUser</code><strong>is</strong><code>undefined</code><strong>or</strong><code>null</code></p>
<p>If you have this issue and are sure that you are logged in before making a request, please update your <code>LIFE_TIME</code> services to <code>TRANSIENT</code>, it should fix the issues :</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// An example with the UserService extended earlier</span>
<span class="hljs-keyword">class</span> UserService <span class="hljs-keyword">extends</span> MedusaUserService {
  <span class="hljs-keyword">static</span> LIFE_TIME = Lifetime.TRANSIENT
  <span class="hljs-comment">// ...</span>
}
</code></pre>
<h2 id="heading-github-branch">GitHub Branch</h2>
<p>You can access the complete part's code <a target="_blank" href="https://git.new/perseides-part-1"><strong>here</strong></a><strong>.</strong></p>
<h2 id="heading-contact">Contact</h2>
<p>You can contact me on Discord and X with the same username : <a target="_blank" href="https://twitter.com/adevinwild">@adevinwild</a></p>
]]></content:encoded></item></channel></rss>