payment-method.service.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import { Injectable } from '@nestjs/common';
  2. import { InjectConnection } from '@nestjs/typeorm';
  3. import { ConfigArg, RefundOrderInput, UpdatePaymentMethodInput } from '@vendure/common/lib/generated-types';
  4. import { omit } from '@vendure/common/lib/omit';
  5. import { ConfigArgType, ID, PaginatedList } from '@vendure/common/lib/shared-types';
  6. import { assertNever } from '@vendure/common/lib/shared-utils';
  7. import { Connection } from 'typeorm';
  8. import { RequestContext } from '../../api/common/request-context';
  9. import { UserInputError } from '../../common/error/errors';
  10. import { ListQueryOptions } from '../../common/types/common-types';
  11. import { ConfigService } from '../../config/config.service';
  12. import { PaymentMethodHandler } from '../../config/payment-method/payment-method-handler';
  13. import { OrderItem } from '../../entity/order-item/order-item.entity';
  14. import { Order } from '../../entity/order/order.entity';
  15. import { PaymentMethod } from '../../entity/payment-method/payment-method.entity';
  16. import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity';
  17. import { Refund } from '../../entity/refund/refund.entity';
  18. import { EventBus } from '../../event-bus/event-bus';
  19. import { PaymentStateTransitionEvent } from '../../event-bus/events/payment-state-transition-event';
  20. import { RefundStateTransitionEvent } from '../../event-bus/events/refund-state-transition-event';
  21. import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
  22. import { PaymentStateMachine } from '../helpers/payment-state-machine/payment-state-machine';
  23. import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
  24. import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
  25. import { patchEntity } from '../helpers/utils/patch-entity';
  26. @Injectable()
  27. export class PaymentMethodService {
  28. constructor(
  29. @InjectConnection() private connection: Connection,
  30. private configService: ConfigService,
  31. private listQueryBuilder: ListQueryBuilder,
  32. private paymentStateMachine: PaymentStateMachine,
  33. private refundStateMachine: RefundStateMachine,
  34. private eventBus: EventBus,
  35. ) {}
  36. async initPaymentMethods() {
  37. await this.ensurePaymentMethodsExist();
  38. }
  39. findAll(options?: ListQueryOptions<PaymentMethod>): Promise<PaginatedList<PaymentMethod>> {
  40. return this.listQueryBuilder
  41. .build(PaymentMethod, options)
  42. .getManyAndCount()
  43. .then(([items, totalItems]) => ({
  44. items,
  45. totalItems,
  46. }));
  47. }
  48. findOne(paymentMethodId: ID): Promise<PaymentMethod | undefined> {
  49. return this.connection.manager.findOne(PaymentMethod, paymentMethodId);
  50. }
  51. async update(input: UpdatePaymentMethodInput): Promise<PaymentMethod> {
  52. const paymentMethod = await getEntityOrThrow(this.connection, PaymentMethod, input.id);
  53. const updatedPaymentMethod = patchEntity(paymentMethod, omit(input, ['configArgs']));
  54. if (input.configArgs) {
  55. const handler = this.configService.paymentOptions.paymentMethodHandlers.find(
  56. h => h.code === paymentMethod.code,
  57. );
  58. if (handler) {
  59. updatedPaymentMethod.configArgs = input.configArgs;
  60. }
  61. }
  62. return this.connection.getRepository(PaymentMethod).save(updatedPaymentMethod);
  63. }
  64. async createPayment(
  65. ctx: RequestContext,
  66. order: Order,
  67. method: string,
  68. metadata: PaymentMetadata,
  69. ): Promise<Payment> {
  70. const { paymentMethod, handler } = await this.getMethodAndHandler(method);
  71. const result = await handler.createPayment(order, paymentMethod.configArgs, metadata || {});
  72. const initialState = 'Created';
  73. const payment = await this.connection
  74. .getRepository(Payment)
  75. .save(new Payment({ ...result, state: initialState }));
  76. await this.paymentStateMachine.transition(ctx, order, payment, result.state);
  77. await this.connection.getRepository(Payment).save(payment, { reload: false });
  78. this.eventBus.publish(
  79. new PaymentStateTransitionEvent(initialState, result.state, ctx, payment, order),
  80. );
  81. return payment;
  82. }
  83. async settlePayment(payment: Payment, order: Order) {
  84. const { paymentMethod, handler } = await this.getMethodAndHandler(payment.method);
  85. return handler.settlePayment(order, payment, paymentMethod.configArgs);
  86. }
  87. async createRefund(
  88. ctx: RequestContext,
  89. input: RefundOrderInput,
  90. order: Order,
  91. items: OrderItem[],
  92. payment: Payment,
  93. ): Promise<Refund> {
  94. const { paymentMethod, handler } = await this.getMethodAndHandler(payment.method);
  95. const itemAmount = items.reduce((sum, item) => sum + item.unitPriceWithTax, 0);
  96. const refundAmount = itemAmount + input.shipping + input.adjustment;
  97. let refund = new Refund({
  98. payment,
  99. orderItems: items,
  100. items: itemAmount,
  101. reason: input.reason,
  102. adjustment: input.adjustment,
  103. shipping: input.shipping,
  104. total: refundAmount,
  105. method: payment.method,
  106. state: 'Pending',
  107. metadata: {},
  108. });
  109. const createRefundResult = await handler.createRefund(
  110. input,
  111. refundAmount,
  112. order,
  113. payment,
  114. paymentMethod.configArgs,
  115. );
  116. if (createRefundResult) {
  117. refund.transactionId = createRefundResult.transactionId || '';
  118. refund.metadata = createRefundResult.metadata || {};
  119. }
  120. refund = await this.connection.getRepository(Refund).save(refund);
  121. if (createRefundResult) {
  122. const fromState = refund.state;
  123. await this.refundStateMachine.transition(ctx, order, refund, createRefundResult.state);
  124. await this.connection.getRepository(Refund).save(refund, { reload: false });
  125. this.eventBus.publish(
  126. new RefundStateTransitionEvent(fromState, createRefundResult.state, ctx, refund, order),
  127. );
  128. }
  129. return refund;
  130. }
  131. getPaymentMethodHandler(code: string): PaymentMethodHandler {
  132. const handler = this.configService.paymentOptions.paymentMethodHandlers.find(h => h.code === code);
  133. if (!handler) {
  134. throw new UserInputError(`error.no-payment-handler-with-code`, { code });
  135. }
  136. return handler;
  137. }
  138. private async getMethodAndHandler(
  139. method: string,
  140. ): Promise<{ paymentMethod: PaymentMethod; handler: PaymentMethodHandler }> {
  141. const paymentMethod = await this.connection.getRepository(PaymentMethod).findOne({
  142. where: {
  143. code: method,
  144. enabled: true,
  145. },
  146. });
  147. if (!paymentMethod) {
  148. throw new UserInputError(`error.payment-method-not-found`, { method });
  149. }
  150. const handler = this.getPaymentMethodHandler(paymentMethod.code);
  151. return { paymentMethod, handler };
  152. }
  153. private async ensurePaymentMethodsExist() {
  154. const paymentMethodHandlers = this.configService.paymentOptions.paymentMethodHandlers;
  155. const existingPaymentMethods = await this.connection.getRepository(PaymentMethod).find();
  156. const toCreate = paymentMethodHandlers.filter(
  157. h => !existingPaymentMethods.find(pm => pm.code === h.code),
  158. );
  159. const toRemove = existingPaymentMethods.filter(
  160. h => !paymentMethodHandlers.find(pm => pm.code === h.code),
  161. );
  162. const toUpdate = existingPaymentMethods.filter(
  163. h => !toCreate.find(x => x.code === h.code) && !toRemove.find(x => x.code === h.code),
  164. );
  165. for (const paymentMethod of toUpdate) {
  166. const handler = paymentMethodHandlers.find(h => h.code === paymentMethod.code);
  167. if (!handler) {
  168. continue;
  169. }
  170. paymentMethod.configArgs = this.buildConfigArgsArray(handler, paymentMethod.configArgs);
  171. await this.connection.getRepository(PaymentMethod).save(paymentMethod, { reload: false });
  172. }
  173. for (const handler of toCreate) {
  174. let paymentMethod = existingPaymentMethods.find(pm => pm.code === handler.code);
  175. if (!paymentMethod) {
  176. paymentMethod = new PaymentMethod({
  177. code: handler.code,
  178. enabled: true,
  179. configArgs: [],
  180. });
  181. }
  182. paymentMethod.configArgs = this.buildConfigArgsArray(handler, paymentMethod.configArgs);
  183. await this.connection.getRepository(PaymentMethod).save(paymentMethod, { reload: false });
  184. }
  185. await this.connection.getRepository(PaymentMethod).remove(toRemove);
  186. }
  187. private buildConfigArgsArray(
  188. handler: PaymentMethodHandler,
  189. existingConfigArgs: ConfigArg[],
  190. ): ConfigArg[] {
  191. let configArgs: ConfigArg[] = [];
  192. for (const [name, def] of Object.entries(handler.args)) {
  193. if (!existingConfigArgs.find(ca => ca.name === name)) {
  194. configArgs.push({
  195. name,
  196. value: this.getDefaultValue(def.type),
  197. });
  198. }
  199. }
  200. configArgs = configArgs.filter(ca => handler.args.hasOwnProperty(ca.name));
  201. return [...existingConfigArgs, ...configArgs];
  202. }
  203. private getDefaultValue(type: ConfigArgType): string {
  204. switch (type) {
  205. case 'string':
  206. return '';
  207. case 'boolean':
  208. return 'false';
  209. case 'int':
  210. case 'float':
  211. return '0';
  212. case 'ID':
  213. return '';
  214. case 'datetime':
  215. return new Date().toISOString();
  216. default:
  217. assertNever(type);
  218. return '';
  219. }
  220. }
  221. }