multivendor.plugin.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { OnApplicationBootstrap } from '@nestjs/common';
  2. import {
  3. Channel,
  4. ChannelService,
  5. configureDefaultOrderProcess,
  6. LanguageCode,
  7. PaymentMethod,
  8. PaymentMethodService,
  9. PluginCommonModule,
  10. RequestContextService,
  11. TransactionalConnection,
  12. VendurePlugin,
  13. } from '@vendure/core';
  14. import { shopApiExtensions } from './api/api-extensions';
  15. import { MultivendorResolver } from './api/mv.resolver';
  16. import { multivendorOrderProcess } from './config/mv-order-process';
  17. import { MultivendorSellerStrategy } from './config/mv-order-seller-strategy';
  18. import { multivendorPaymentMethodHandler } from './config/mv-payment-handler';
  19. import { multivendorShippingEligibilityChecker } from './config/mv-shipping-eligibility-checker';
  20. import { MultivendorShippingLineAssignmentStrategy } from './config/mv-shipping-line-assignment-strategy';
  21. import { CONNECTED_PAYMENT_METHOD_CODE, MULTIVENDOR_PLUGIN_OPTIONS } from './constants';
  22. import { MultivendorService } from './service/mv.service';
  23. import { MultivendorPluginOptions } from './types';
  24. /**
  25. * @description
  26. * This is an example of how to implement a multivendor marketplace app using the new features introduced in
  27. * Vendure v2.0.
  28. *
  29. * ## Setup
  30. *
  31. * Add this plugin to your VendureConfig:
  32. * ```ts
  33. * plugins: [
  34. * MultivendorPlugin.init({
  35. * platformFeePercent: 10,
  36. * platformFeeSKU: 'FEE',
  37. * }),
  38. * // ...
  39. * ]
  40. * ```
  41. *
  42. * ## Create a Seller
  43. *
  44. * Now you can create new sellers with the following mutation:
  45. *
  46. * ```graphql
  47. * mutation RegisterSeller {
  48. * registerNewSeller(input: {
  49. * shopName: "Bob's Parts",
  50. * seller {
  51. * firstName: "Bob"
  52. * lastName: "Dobalina"
  53. * emailAddress: "bob@bobs-parts.com"
  54. * password: "test",
  55. * }
  56. * }) {
  57. * id
  58. * code
  59. * token
  60. * }
  61. * }
  62. * ```
  63. *
  64. * This mutation will:
  65. *
  66. * - Create a new Seller representing the shop "Bob's Parts"
  67. * - Create a new Channel and associate it with the new Seller
  68. * - Create a Role & Administrator for Bob to access his shop admin account
  69. * - Create a ShippingMethod for Bob's shop
  70. * - Create a StockLocation for Bob's shop
  71. *
  72. * Bob can then go and sign in to the Admin UI using the provided emailAddress & password credentials, and start
  73. * creating some products.
  74. *
  75. * Repeat this process for more Sellers.
  76. *
  77. * ## Storefront
  78. *
  79. * To create a multivendor Order, use the default Channel in the storefront and add variants to an Order from
  80. * various Sellers.
  81. *
  82. * ### Shipping
  83. *
  84. * When it comes to setting the shipping method, the `eligibleShippingMethods` query should just return the
  85. * shipping methods for the shops from which the OrderLines come. So assuming the Order contains items from 3 different
  86. * Sellers, there should be at least 3 eligible ShippingMethods (plus any global ones from the default Channel).
  87. *
  88. * You should now select the IDs of all the Seller-specific ShippingMethods:
  89. *
  90. * ```graphql
  91. * mutation {
  92. * setOrderShippingMethod(shippingMethodId: ["3", "4"]) {
  93. * ... on Order {
  94. * id
  95. * }
  96. * }
  97. * }
  98. * ```
  99. *
  100. * ### Payment
  101. *
  102. * This plugin automatically creates a "connected payment method" in the default Channel, which is a simple simulation
  103. * of something like Stripe Connect.
  104. *
  105. * ```graphql
  106. * mutation {
  107. * addPaymentToOrder(input: { method: "connected-payment-method", metadata: {} }) {
  108. * ... on Order { id }
  109. * ... on ErrorResult {
  110. * errorCode
  111. * message
  112. * }
  113. * ... on PaymentFailedError {
  114. * paymentErrorMessage
  115. * }
  116. * }
  117. * }
  118. * ```
  119. *
  120. * After that, you should be able to see that the Order has been split into an "aggregate" order in the default Channel,
  121. * and then one or more "seller" orders in each Channel from which the customer bought items.
  122. */
  123. @VendurePlugin({
  124. imports: [PluginCommonModule],
  125. configuration: config => {
  126. config.customFields.Seller.push({
  127. name: 'connectedAccountId',
  128. label: [{ languageCode: LanguageCode.en, value: 'Connected account ID' }],
  129. description: [
  130. { languageCode: LanguageCode.en, value: 'The ID used to process connected payments' },
  131. ],
  132. type: 'string',
  133. public: false,
  134. });
  135. config.paymentOptions.paymentMethodHandlers.push(multivendorPaymentMethodHandler);
  136. const customDefaultOrderProcess = configureDefaultOrderProcess({
  137. checkFulfillmentStates: false,
  138. });
  139. config.orderOptions.process = [customDefaultOrderProcess, multivendorOrderProcess];
  140. config.orderOptions.orderSellerStrategy = new MultivendorSellerStrategy();
  141. config.shippingOptions.shippingEligibilityCheckers.push(multivendorShippingEligibilityChecker);
  142. config.shippingOptions.shippingLineAssignmentStrategy =
  143. new MultivendorShippingLineAssignmentStrategy();
  144. return config;
  145. },
  146. shopApiExtensions: {
  147. schema: shopApiExtensions,
  148. resolvers: [MultivendorResolver],
  149. },
  150. providers: [
  151. MultivendorService,
  152. { provide: MULTIVENDOR_PLUGIN_OPTIONS, useFactory: () => MultivendorPlugin.options },
  153. ],
  154. })
  155. export class MultivendorPlugin implements OnApplicationBootstrap {
  156. static options: MultivendorPluginOptions;
  157. constructor(
  158. private connection: TransactionalConnection,
  159. private channelService: ChannelService,
  160. private requestContextService: RequestContextService,
  161. private paymentMethodService: PaymentMethodService,
  162. ) {}
  163. static init(options: MultivendorPluginOptions) {
  164. MultivendorPlugin.options = options;
  165. return MultivendorPlugin;
  166. }
  167. async onApplicationBootstrap() {
  168. await this.ensureConnectedPaymentMethodExists();
  169. }
  170. private async ensureConnectedPaymentMethodExists() {
  171. const paymentMethod = await this.connection.rawConnection.getRepository(PaymentMethod).findOne({
  172. where: {
  173. code: CONNECTED_PAYMENT_METHOD_CODE,
  174. },
  175. });
  176. if (!paymentMethod) {
  177. const ctx = await this.requestContextService.create({ apiType: 'admin' });
  178. const allChannels = await this.connection.getRepository(ctx, Channel).find();
  179. const createdPaymentMethod = await this.paymentMethodService.create(ctx, {
  180. code: CONNECTED_PAYMENT_METHOD_CODE,
  181. enabled: true,
  182. handler: {
  183. code: multivendorPaymentMethodHandler.code,
  184. arguments: [],
  185. },
  186. translations: [
  187. {
  188. languageCode: LanguageCode.en,
  189. name: 'Connected Payments',
  190. },
  191. ],
  192. });
  193. await this.channelService.assignToChannels(
  194. ctx,
  195. PaymentMethod,
  196. createdPaymentMethod.id,
  197. allChannels.map(c => c.id),
  198. );
  199. }
  200. }
  201. }