multivendor.plugin.ts 7.1 KB

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