From 3f72311635b353d1f6e393638a0c5d59ee1b4fec Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 6 Oct 2020 14:14:50 +0200 Subject: [PATCH] feat(core): Allow public & private Payment metadata Closes #476 BREAKING CHANGE: The `Payment.metadata` field is not private by default, meaning that it can only be read via the Admin API. Data required in the Shop API can be accessed by putting it in a field named `public`. Example: `Payment.metadata.public.redirectUrl` --- .../payment-integrations/index.md | 11 +- .../lib/core/src/common/generated-types.ts | 22 + .../core/e2e/fixtures/test-payment-methods.ts | 19 +- .../e2e/graphql/generated-e2e-admin-types.ts | 493 +++++++++--------- .../e2e/graphql/generated-e2e-shop-types.ts | 12 + packages/core/e2e/graphql/shop-definitions.ts | 9 + packages/core/e2e/order.e2e-spec.ts | 65 ++- packages/core/e2e/shop-order.e2e-spec.ts | 4 +- packages/core/e2e/utils/test-order-utils.ts | 3 +- .../entity/payment-entity.resolver.ts | 9 + packages/core/src/common/index.ts | 3 + .../core/src/common/types/common-types.ts | 6 + .../example-payment-method-handler.ts | 4 +- .../payment-method/payment-method-handler.ts | 31 +- .../payment-method/payment-method.entity.ts | 4 - .../core/src/entity/payment/payment.entity.ts | 7 +- .../core/src/entity/refund/refund.entity.ts | 3 +- .../src/service/services/order.service.ts | 22 +- .../services/payment-method.service.ts | 9 +- 19 files changed, 463 insertions(+), 273 deletions(-) diff --git a/docs/content/docs/developer-guide/payment-integrations/index.md b/docs/content/docs/developer-guide/payment-integrations/index.md index eabc385b3c..469ffb8455 100644 --- a/docs/content/docs/developer-guide/payment-integrations/index.md +++ b/docs/content/docs/developer-guide/payment-integrations/index.md @@ -57,7 +57,16 @@ const myPaymentIntegration = new PaymentMethodHandler({ amount: order.total, state: 'Authorized' as const, transactionId: result.id.toString(), - metadata: result.outcome, + metadata: { + cardInfo: result.cardInfo, + // Any metadata in the `public` field + // will be available in the Shop API, + // All other metadata is private and + // only available in the Admin API. + public: { + referenceCode: result.publicId, + } + }, }; } catch (err) { return { diff --git a/packages/admin-ui/src/lib/core/src/common/generated-types.ts b/packages/admin-ui/src/lib/core/src/common/generated-types.ts index 848e87582c..5e5d8fe948 100644 --- a/packages/admin-ui/src/lib/core/src/common/generated-types.ts +++ b/packages/admin-ui/src/lib/core/src/common/generated-types.ts @@ -4396,6 +4396,20 @@ export type GetUiStateQuery = { uiState: ( & Pick ) }; +export type GetClientStateQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetClientStateQuery = { networkStatus: ( + { __typename?: 'NetworkStatus' } + & Pick + ), userStatus: ( + { __typename?: 'UserStatus' } + & UserStatusFragment + ), uiState: ( + { __typename?: 'UiState' } + & Pick + ) }; + export type SetActiveChannelMutationVariables = Exact<{ channelId: Scalars['ID']; }>; @@ -7077,6 +7091,14 @@ export namespace GetUiState { export type UiState = (NonNullable); } +export namespace GetClientState { + export type Variables = GetClientStateQueryVariables; + export type Query = GetClientStateQuery; + export type NetworkStatus = (NonNullable); + export type UserStatus = (NonNullable); + export type UiState = (NonNullable); +} + export namespace SetActiveChannel { export type Variables = SetActiveChannelMutationVariables; export type Mutation = SetActiveChannelMutation; diff --git a/packages/core/e2e/fixtures/test-payment-methods.ts b/packages/core/e2e/fixtures/test-payment-methods.ts index a888a558da..ba684ef8d9 100644 --- a/packages/core/e2e/fixtures/test-payment-methods.ts +++ b/packages/core/e2e/fixtures/test-payment-methods.ts @@ -11,7 +11,7 @@ export const testSuccessfulPaymentMethod = new PaymentMethodHandler({ amount: order.total, state: 'Settled', transactionId: '12345', - metadata, + metadata: { public: metadata }, }; }, settlePayment: order => ({ @@ -32,7 +32,7 @@ export const twoStagePaymentMethod = new PaymentMethodHandler({ amount: order.total, state: 'Authorized', transactionId: '12345', - metadata, + metadata: { public: metadata }, }; }, settlePayment: () => { @@ -87,13 +87,24 @@ export const failsToSettlePaymentMethod = new PaymentMethodHandler({ amount: order.total, state: 'Authorized', transactionId: '12345', - metadata, + metadata: { + privateCreatePaymentData: 'secret', + public: { + publicCreatePaymentData: 'public', + }, + }, }; }, settlePayment: () => { return { success: false, errorMessage: 'Something went horribly wrong', + metadata: { + privateSettlePaymentData: 'secret', + public: { + publicSettlePaymentData: 'public', + }, + }, }; }, }); @@ -106,7 +117,7 @@ export const testFailingPaymentMethod = new PaymentMethodHandler({ amount: order.total, state: 'Declined', errorMessage: 'Insufficient funds', - metadata, + metadata: { public: metadata }, }; }, settlePayment: order => ({ diff --git a/packages/core/e2e/graphql/generated-e2e-admin-types.ts b/packages/core/e2e/graphql/generated-e2e-admin-types.ts index b6113bc344..14d3fd102d 100644 --- a/packages/core/e2e/graphql/generated-e2e-admin-types.ts +++ b/packages/core/e2e/graphql/generated-e2e-admin-types.ts @@ -5035,125 +5035,6 @@ export type GetPromoProductsQuery = { }; }; -export type SettlePaymentMutationVariables = Exact<{ - id: Scalars['ID']; -}>; - -export type SettlePaymentMutation = { - settlePayment: - | PaymentFragment - | Pick - | Pick - | Pick; -}; - -export type PaymentFragment = Pick; - -export type GetOrderListFulfillmentsQueryVariables = Exact<{ [key: string]: never }>; - -export type GetOrderListFulfillmentsQuery = { - orders: { - items: Array< - Pick & { - fulfillments?: Maybe>>; - } - >; - }; -}; - -export type GetOrderFulfillmentItemsQueryVariables = Exact<{ - id: Scalars['ID']; -}>; - -export type GetOrderFulfillmentItemsQuery = { - order?: Maybe & { fulfillments?: Maybe> }>; -}; - -export type CancelOrderMutationVariables = Exact<{ - input: CancelOrderInput; -}>; - -export type CancelOrderMutation = { - cancelOrder: - | CanceledOrderFragment - | Pick - | Pick - | Pick - | Pick - | Pick; -}; - -export type CanceledOrderFragment = Pick & { - lines: Array & { items: Array> }>; -}; - -export type RefundFragment = Pick< - Refund, - 'id' | 'state' | 'items' | 'transactionId' | 'shipping' | 'total' | 'metadata' ->; - -export type RefundOrderMutationVariables = Exact<{ - input: RefundOrderInput; -}>; - -export type RefundOrderMutation = { - refundOrder: - | RefundFragment - | Pick - | Pick - | Pick - | Pick - | Pick - | Pick - | Pick - | Pick; -}; - -export type SettleRefundMutationVariables = Exact<{ - input: SettleRefundInput; -}>; - -export type SettleRefundMutation = { - settleRefund: RefundFragment | Pick; -}; - -export type GetOrderHistoryQueryVariables = Exact<{ - id: Scalars['ID']; - options?: Maybe; -}>; - -export type GetOrderHistoryQuery = { - order?: Maybe< - Pick & { - history: Pick & { - items: Array< - Pick & { - administrator?: Maybe>; - } - >; - }; - } - >; -}; - -export type AddNoteToOrderMutationVariables = Exact<{ - input: AddNoteToOrderInput; -}>; - -export type AddNoteToOrderMutation = { addNoteToOrder: Pick }; - -export type UpdateOrderNoteMutationVariables = Exact<{ - input: UpdateOrderNoteInput; -}>; - -export type UpdateOrderNoteMutation = { updateOrderNote: Pick }; - -export type DeleteOrderNoteMutationVariables = Exact<{ - id: Scalars['ID']; -}>; - -export type DeleteOrderNoteMutation = { deleteOrderNote: Pick }; - export type ProductOptionGroupFragment = Pick & { options: Array>; translations: Array>; @@ -5501,6 +5382,135 @@ export type DeleteTaxRateMutationVariables = Exact<{ export type DeleteTaxRateMutation = { deleteTaxRate: Pick }; +export type SettlePaymentMutationVariables = Exact<{ + id: Scalars['ID']; +}>; + +export type SettlePaymentMutation = { + settlePayment: + | PaymentFragment + | Pick + | Pick + | Pick; +}; + +export type PaymentFragment = Pick; + +export type GetOrderListFulfillmentsQueryVariables = Exact<{ [key: string]: never }>; + +export type GetOrderListFulfillmentsQuery = { + orders: { + items: Array< + Pick & { + fulfillments?: Maybe>>; + } + >; + }; +}; + +export type GetOrderFulfillmentItemsQueryVariables = Exact<{ + id: Scalars['ID']; +}>; + +export type GetOrderFulfillmentItemsQuery = { + order?: Maybe & { fulfillments?: Maybe> }>; +}; + +export type CancelOrderMutationVariables = Exact<{ + input: CancelOrderInput; +}>; + +export type CancelOrderMutation = { + cancelOrder: + | CanceledOrderFragment + | Pick + | Pick + | Pick + | Pick + | Pick; +}; + +export type CanceledOrderFragment = Pick & { + lines: Array & { items: Array> }>; +}; + +export type RefundFragment = Pick< + Refund, + 'id' | 'state' | 'items' | 'transactionId' | 'shipping' | 'total' | 'metadata' +>; + +export type RefundOrderMutationVariables = Exact<{ + input: RefundOrderInput; +}>; + +export type RefundOrderMutation = { + refundOrder: + | RefundFragment + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick + | Pick; +}; + +export type SettleRefundMutationVariables = Exact<{ + input: SettleRefundInput; +}>; + +export type SettleRefundMutation = { + settleRefund: RefundFragment | Pick; +}; + +export type GetOrderHistoryQueryVariables = Exact<{ + id: Scalars['ID']; + options?: Maybe; +}>; + +export type GetOrderHistoryQuery = { + order?: Maybe< + Pick & { + history: Pick & { + items: Array< + Pick & { + administrator?: Maybe>; + } + >; + }; + } + >; +}; + +export type AddNoteToOrderMutationVariables = Exact<{ + input: AddNoteToOrderInput; +}>; + +export type AddNoteToOrderMutation = { addNoteToOrder: Pick }; + +export type UpdateOrderNoteMutationVariables = Exact<{ + input: UpdateOrderNoteInput; +}>; + +export type UpdateOrderNoteMutation = { updateOrderNote: Pick }; + +export type DeleteOrderNoteMutationVariables = Exact<{ + id: Scalars['ID']; +}>; + +export type DeleteOrderNoteMutation = { deleteOrderNote: Pick }; + +export type GetOrderWithPaymentsQueryVariables = Exact<{ + id: Scalars['ID']; +}>; + +export type GetOrderWithPaymentsQuery = { + order?: Maybe< + Pick & { payments?: Maybe>> } + >; +}; + export type DeleteZoneMutationVariables = Exact<{ id: Scalars['ID']; }>; @@ -6840,124 +6850,6 @@ export namespace GetPromoProducts { >; } -export namespace SettlePayment { - export type Variables = SettlePaymentMutationVariables; - export type Mutation = SettlePaymentMutation; - export type SettlePayment = NonNullable; - export type ErrorResultInlineFragment = DiscriminateUnion< - NonNullable, - { __typename?: 'ErrorResult' } - >; - export type SettlePaymentErrorInlineFragment = DiscriminateUnion< - NonNullable, - { __typename?: 'SettlePaymentError' } - >; -} - -export namespace Payment { - export type Fragment = PaymentFragment; -} - -export namespace GetOrderListFulfillments { - export type Variables = GetOrderListFulfillmentsQueryVariables; - export type Query = GetOrderListFulfillmentsQuery; - export type Orders = NonNullable; - export type Items = NonNullable< - NonNullable['items']>[number] - >; - export type Fulfillments = NonNullable< - NonNullable< - NonNullable< - NonNullable['items']>[number] - >['fulfillments'] - >[number] - >; -} - -export namespace GetOrderFulfillmentItems { - export type Variables = GetOrderFulfillmentItemsQueryVariables; - export type Query = GetOrderFulfillmentItemsQuery; - export type Order = NonNullable; - export type Fulfillments = NonNullable< - NonNullable['fulfillments']>[number] - >; -} - -export namespace CancelOrder { - export type Variables = CancelOrderMutationVariables; - export type Mutation = CancelOrderMutation; - export type CancelOrder = NonNullable; - export type ErrorResultInlineFragment = DiscriminateUnion< - NonNullable, - { __typename?: 'ErrorResult' } - >; -} - -export namespace CanceledOrder { - export type Fragment = CanceledOrderFragment; - export type Lines = NonNullable[number]>; - export type Items = NonNullable< - NonNullable[number]>['items']>[number] - >; -} - -export namespace Refund { - export type Fragment = RefundFragment; -} - -export namespace RefundOrder { - export type Variables = RefundOrderMutationVariables; - export type Mutation = RefundOrderMutation; - export type RefundOrder = NonNullable; - export type ErrorResultInlineFragment = DiscriminateUnion< - NonNullable, - { __typename?: 'ErrorResult' } - >; -} - -export namespace SettleRefund { - export type Variables = SettleRefundMutationVariables; - export type Mutation = SettleRefundMutation; - export type SettleRefund = NonNullable; - export type ErrorResultInlineFragment = DiscriminateUnion< - NonNullable, - { __typename?: 'ErrorResult' } - >; -} - -export namespace GetOrderHistory { - export type Variables = GetOrderHistoryQueryVariables; - export type Query = GetOrderHistoryQuery; - export type Order = NonNullable; - export type History = NonNullable['history']>; - export type Items = NonNullable< - NonNullable['history']>['items']>[number] - >; - export type Administrator = NonNullable< - NonNullable< - NonNullable['history']>['items']>[number] - >['administrator'] - >; -} - -export namespace AddNoteToOrder { - export type Variables = AddNoteToOrderMutationVariables; - export type Mutation = AddNoteToOrderMutation; - export type AddNoteToOrder = NonNullable; -} - -export namespace UpdateOrderNote { - export type Variables = UpdateOrderNoteMutationVariables; - export type Mutation = UpdateOrderNoteMutation; - export type UpdateOrderNote = NonNullable; -} - -export namespace DeleteOrderNote { - export type Variables = DeleteOrderNoteMutationVariables; - export type Mutation = DeleteOrderNoteMutation; - export type DeleteOrderNote = NonNullable; -} - export namespace ProductOptionGroup { export type Fragment = ProductOptionGroupFragment; export type Options = NonNullable[number]>; @@ -7365,6 +7257,133 @@ export namespace DeleteTaxRate { export type DeleteTaxRate = NonNullable; } +export namespace SettlePayment { + export type Variables = SettlePaymentMutationVariables; + export type Mutation = SettlePaymentMutation; + export type SettlePayment = NonNullable; + export type ErrorResultInlineFragment = DiscriminateUnion< + NonNullable, + { __typename?: 'ErrorResult' } + >; + export type SettlePaymentErrorInlineFragment = DiscriminateUnion< + NonNullable, + { __typename?: 'SettlePaymentError' } + >; +} + +export namespace Payment { + export type Fragment = PaymentFragment; +} + +export namespace GetOrderListFulfillments { + export type Variables = GetOrderListFulfillmentsQueryVariables; + export type Query = GetOrderListFulfillmentsQuery; + export type Orders = NonNullable; + export type Items = NonNullable< + NonNullable['items']>[number] + >; + export type Fulfillments = NonNullable< + NonNullable< + NonNullable< + NonNullable['items']>[number] + >['fulfillments'] + >[number] + >; +} + +export namespace GetOrderFulfillmentItems { + export type Variables = GetOrderFulfillmentItemsQueryVariables; + export type Query = GetOrderFulfillmentItemsQuery; + export type Order = NonNullable; + export type Fulfillments = NonNullable< + NonNullable['fulfillments']>[number] + >; +} + +export namespace CancelOrder { + export type Variables = CancelOrderMutationVariables; + export type Mutation = CancelOrderMutation; + export type CancelOrder = NonNullable; + export type ErrorResultInlineFragment = DiscriminateUnion< + NonNullable, + { __typename?: 'ErrorResult' } + >; +} + +export namespace CanceledOrder { + export type Fragment = CanceledOrderFragment; + export type Lines = NonNullable[number]>; + export type Items = NonNullable< + NonNullable[number]>['items']>[number] + >; +} + +export namespace Refund { + export type Fragment = RefundFragment; +} + +export namespace RefundOrder { + export type Variables = RefundOrderMutationVariables; + export type Mutation = RefundOrderMutation; + export type RefundOrder = NonNullable; + export type ErrorResultInlineFragment = DiscriminateUnion< + NonNullable, + { __typename?: 'ErrorResult' } + >; +} + +export namespace SettleRefund { + export type Variables = SettleRefundMutationVariables; + export type Mutation = SettleRefundMutation; + export type SettleRefund = NonNullable; + export type ErrorResultInlineFragment = DiscriminateUnion< + NonNullable, + { __typename?: 'ErrorResult' } + >; +} + +export namespace GetOrderHistory { + export type Variables = GetOrderHistoryQueryVariables; + export type Query = GetOrderHistoryQuery; + export type Order = NonNullable; + export type History = NonNullable['history']>; + export type Items = NonNullable< + NonNullable['history']>['items']>[number] + >; + export type Administrator = NonNullable< + NonNullable< + NonNullable['history']>['items']>[number] + >['administrator'] + >; +} + +export namespace AddNoteToOrder { + export type Variables = AddNoteToOrderMutationVariables; + export type Mutation = AddNoteToOrderMutation; + export type AddNoteToOrder = NonNullable; +} + +export namespace UpdateOrderNote { + export type Variables = UpdateOrderNoteMutationVariables; + export type Mutation = UpdateOrderNoteMutation; + export type UpdateOrderNote = NonNullable; +} + +export namespace DeleteOrderNote { + export type Variables = DeleteOrderNoteMutationVariables; + export type Mutation = DeleteOrderNoteMutation; + export type DeleteOrderNote = NonNullable; +} + +export namespace GetOrderWithPayments { + export type Variables = GetOrderWithPaymentsQueryVariables; + export type Query = GetOrderWithPaymentsQuery; + export type Order = NonNullable; + export type Payments = NonNullable< + NonNullable['payments']>[number] + >; +} + export namespace DeleteZone { export type Variables = DeleteZoneMutationVariables; export type Mutation = DeleteZoneMutation; diff --git a/packages/core/e2e/graphql/generated-e2e-shop-types.ts b/packages/core/e2e/graphql/generated-e2e-shop-types.ts index 3ce28f9b7f..87d9b27a1d 100644 --- a/packages/core/e2e/graphql/generated-e2e-shop-types.ts +++ b/packages/core/e2e/graphql/generated-e2e-shop-types.ts @@ -2925,6 +2925,12 @@ export type GetActiveOrderPaymentsQuery = { >; }; +export type GetOrderByCodeWithPaymentsQueryVariables = Exact<{ + code: Scalars['String']; +}>; + +export type GetOrderByCodeWithPaymentsQuery = { orderByCode?: Maybe }; + export type GetNextOrderStatesQueryVariables = Exact<{ [key: string]: never }>; export type GetNextOrderStatesQuery = Pick; @@ -3336,6 +3342,12 @@ export namespace GetActiveOrderPayments { >; } +export namespace GetOrderByCodeWithPayments { + export type Variables = GetOrderByCodeWithPaymentsQueryVariables; + export type Query = GetOrderByCodeWithPaymentsQuery; + export type OrderByCode = NonNullable; +} + export namespace GetNextOrderStates { export type Variables = GetNextOrderStatesQueryVariables; export type Query = GetNextOrderStatesQuery; diff --git a/packages/core/e2e/graphql/shop-definitions.ts b/packages/core/e2e/graphql/shop-definitions.ts index e75a4f42b3..26d8c44a6c 100644 --- a/packages/core/e2e/graphql/shop-definitions.ts +++ b/packages/core/e2e/graphql/shop-definitions.ts @@ -510,6 +510,15 @@ export const GET_ACTIVE_ORDER_PAYMENTS = gql` } `; +export const GET_ORDER_BY_CODE_WITH_PAYMENTS = gql` + query GetOrderByCodeWithPayments($code: String!) { + orderByCode(code: $code) { + ...TestOrderWithPayments + } + } + ${TEST_ORDER_WITH_PAYMENTS_FRAGMENT} +`; + export const GET_NEXT_STATES = gql` query GetNextOrderStates { nextOrderStates diff --git a/packages/core/e2e/order.e2e-spec.ts b/packages/core/e2e/order.e2e-spec.ts index 4a0d6fb4f6..87bcc58265 100644 --- a/packages/core/e2e/order.e2e-spec.ts +++ b/packages/core/e2e/order.e2e-spec.ts @@ -34,6 +34,7 @@ import { GetOrderHistory, GetOrderList, GetOrderListFulfillments, + GetOrderWithPayments, GetProductWithVariants, GetStockMovement, HistoryEntryType, @@ -52,6 +53,9 @@ import { AddItemToOrder, DeletionResult, GetActiveOrder, + GetActiveOrderWithPayments, + GetOrderByCode, + GetOrderByCodeWithPayments, TestOrderFragmentFragment, UpdatedOrder, } from './graphql/generated-e2e-shop-types'; @@ -66,7 +70,14 @@ import { TRANSIT_FULFILLMENT, UPDATE_PRODUCT_VARIANTS, } from './graphql/shared-definitions'; -import { ADD_ITEM_TO_ORDER, GET_ACTIVE_ORDER } from './graphql/shop-definitions'; +import { + ADD_ITEM_TO_ORDER, + GET_ACTIVE_ORDER, + GET_ACTIVE_ORDER_WITH_PAYMENTS, + GET_ORDER_BY_CODE, + GET_ORDER_BY_CODE_WITH_PAYMENTS, + TEST_ORDER_WITH_PAYMENTS_FRAGMENT, +} from './graphql/shop-definitions'; import { assertThrowsWithMessage } from './utils/assert-throws-with-message'; import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils'; @@ -159,6 +170,9 @@ describe('Orders resolver', () => { }); describe('payments', () => { + let firstOrderCode: string; + let firstOrderId: string; + it('settlePayment fails', async () => { await shopClient.asUserWithCredentials(customers[0].emailAddress, password); await proceedToArrangingPayment(shopClient); @@ -185,6 +199,38 @@ describe('Orders resolver', () => { }); expect(result.order!.state).toBe('PaymentAuthorized'); + firstOrderCode = order.code; + firstOrderId = order.id; + }); + + it('public payment metadata available in Shop API', async () => { + const { orderByCode } = await shopClient.query< + GetOrderByCodeWithPayments.Query, + GetOrderByCodeWithPayments.Variables + >(GET_ORDER_BY_CODE_WITH_PAYMENTS, { code: firstOrderCode }); + + expect(orderByCode?.payments?.[0].metadata).toEqual({ + public: { + publicCreatePaymentData: 'public', + publicSettlePaymentData: 'public', + }, + }); + }); + + it('public and private payment metadata available in Admin API', async () => { + const { order } = await adminClient.query< + GetOrderWithPayments.Query, + GetOrderWithPayments.Variables + >(GET_ORDER_WITH_PAYMENTS, { id: firstOrderId }); + + expect(order?.payments?.[0].metadata).toEqual({ + privateCreatePaymentData: 'secret', + privateSettlePaymentData: 'secret', + public: { + publicCreatePaymentData: 'public', + publicSettlePaymentData: 'public', + }, + }); }); it('settlePayment succeeds, onStateTransitionStart called', async () => { @@ -212,8 +258,10 @@ describe('Orders resolver', () => { expect(settlePayment!.state).toBe('Settled'); // further metadata is combined into existing object expect(settlePayment!.metadata).toEqual({ - baz: 'quux', moreData: 42, + public: { + baz: 'quux', + }, }); expect(onTransitionSpy).toHaveBeenCalledTimes(2); expect(onTransitionSpy.mock.calls[1][0]).toBe('Authorized'); @@ -1613,3 +1661,16 @@ export const DELETE_ORDER_NOTE = gql` } } `; + +const GET_ORDER_WITH_PAYMENTS = gql` + query GetOrderWithPayments($id: ID!) { + order(id: $id) { + id + payments { + id + errorMessage + metadata + } + } + } +`; diff --git a/packages/core/e2e/shop-order.e2e-spec.ts b/packages/core/e2e/shop-order.e2e-spec.ts index 1f8f220a00..bf480eefe4 100644 --- a/packages/core/e2e/shop-order.e2e-spec.ts +++ b/packages/core/e2e/shop-order.e2e-spec.ts @@ -943,7 +943,7 @@ describe('Shop orders', () => { expect(payment.state).toBe('Declined'); expect(payment.transactionId).toBe(null); expect(payment.metadata).toEqual({ - foo: 'bar', + public: { foo: 'bar' }, }); }); @@ -997,7 +997,7 @@ describe('Shop orders', () => { expect(payment.state).toBe('Settled'); expect(payment.transactionId).toBe('12345'); expect(payment.metadata).toEqual({ - baz: 'quux', + public: { baz: 'quux' }, }); }); diff --git a/packages/core/e2e/utils/test-order-utils.ts b/packages/core/e2e/utils/test-order-utils.ts index 9f0271d2fb..f2caad69cc 100644 --- a/packages/core/e2e/utils/test-order-utils.ts +++ b/packages/core/e2e/utils/test-order-utils.ts @@ -8,6 +8,7 @@ import { GetShippingMethods, SetShippingAddress, SetShippingMethod, + TestOrderFragmentFragment, TransitionToState, } from '../graphql/generated-e2e-shop-types'; import { @@ -42,7 +43,7 @@ export async function proceedToArrangingPayment(shopClient: SimpleGraphQLClient) TransitionToState.Variables >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' }); - return transitionOrderToState!.id; + return (transitionOrderToState as TestOrderFragmentFragment)!.id; } export async function addPaymentToOrder( diff --git a/packages/core/src/api/resolvers/entity/payment-entity.resolver.ts b/packages/core/src/api/resolvers/entity/payment-entity.resolver.ts index e101bd2790..2db22473ee 100644 --- a/packages/core/src/api/resolvers/entity/payment-entity.resolver.ts +++ b/packages/core/src/api/resolvers/entity/payment-entity.resolver.ts @@ -1,9 +1,13 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { pick } from '@vendure/common/lib/pick'; +import { PaymentMetadata } from '../../../common/types/common-types'; import { Payment } from '../../../entity/payment/payment.entity'; import { Refund } from '../../../entity/refund/refund.entity'; import { OrderService } from '../../../service/services/order.service'; +import { ApiType } from '../../common/get-api-type'; import { RequestContext } from '../../common/request-context'; +import { Api } from '../../decorators/api.decorator'; import { Ctx } from '../../decorators/request-context.decorator'; @Resolver('Payment') @@ -18,4 +22,9 @@ export class PaymentEntityResolver { return this.orderService.getPaymentRefunds(ctx, payment.id); } } + + @ResolveField() + metadata(@Api() apiType: ApiType, @Parent() payment: Payment): PaymentMetadata { + return apiType === 'admin' ? payment.metadata : pick(payment.metadata, ['public']); + } } diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 201e97b69a..10162cd55b 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -6,4 +6,7 @@ export * from './error/error-result'; export * from './error/generated-graphql-admin-errors'; export * from './injector'; export * from './ttl-cache'; +export * from './types/common-types'; +export * from './types/injectable-strategy'; +export * from './types/locale-types'; export * from './utils'; diff --git a/packages/core/src/common/types/common-types.ts b/packages/core/src/common/types/common-types.ts index d813350cfe..cba2202f41 100644 --- a/packages/core/src/common/types/common-types.ts +++ b/packages/core/src/common/types/common-types.ts @@ -113,3 +113,9 @@ export interface DateOperators { after?: Date; between?: DateRange; } + +export type PaymentMetadata = { + [prop: string]: any; +} & { + public?: any; +}; diff --git a/packages/core/src/config/payment-method/example-payment-method-handler.ts b/packages/core/src/config/payment-method/example-payment-method-handler.ts index 6b754b7900..6e7c04dae9 100644 --- a/packages/core/src/config/payment-method/example-payment-method-handler.ts +++ b/packages/core/src/config/payment-method/example-payment-method-handler.ts @@ -9,9 +9,7 @@ const gripeSDK = { charges: { create: (options: any) => { return Promise.resolve({ - id: Math.random() - .toString(36) - .substr(3), + id: Math.random().toString(36).substr(3), }); }, capture: (transactionId: string) => { diff --git a/packages/core/src/config/payment-method/payment-method-handler.ts b/packages/core/src/config/payment-method/payment-method-handler.ts index 2ef00db3b8..05e5ef4518 100644 --- a/packages/core/src/config/payment-method/payment-method-handler.ts +++ b/packages/core/src/config/payment-method/payment-method-handler.ts @@ -7,8 +7,9 @@ import { ConfigurableOperationDefOptions, } from '../../common/configurable-operation'; import { OnTransitionStartFn, StateMachineConfig } from '../../common/finite-state-machine/types'; +import { PaymentMetadata } from '../../common/types/common-types'; import { Order } from '../../entity/order/order.entity'; -import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity'; +import { Payment } from '../../entity/payment/payment.entity'; import { PaymentState, PaymentTransitionData, @@ -27,8 +28,21 @@ export type OnPaymentTransitionStartReturnType = ReturnType< * @docsPage Payment Method Types */ export interface CreatePaymentResult { + /** + * @description + * The amount (as an integer - i.e. $10 = `1000`) that this payment is for. + * Typically this should equal the Order total, unless multiple payment methods + * are being used for the order. + */ amount: number; - state: Exclude; + /** + * @description + * The {@link PaymentState} of the resulting Payment. + * + * In a single-step payment flow, this should be set to `'Settled'`. + * In a two-step flow, this should be set to `'Authorized'`. + */ + state: Exclude; /** * @description * The unique payment reference code typically assigned by @@ -45,7 +59,13 @@ export interface CreatePaymentResult { /** * @description * This field can be used to store other relevant data which is often - * provided by the payment provider. + * provided by the payment provider, such as security data related to + * the payment method or data used in troubleshooting or debugging. + * + * Any data stored in the optional `public` property will be available + * via the Shop API. This is useful for certain checkout flows such as + * external gateways, where the payment provider returns a unique + * url which must then be passed to the storefront app. */ metadata?: PaymentMetadata; } @@ -95,6 +115,8 @@ export interface SettlePaymentResult { * @description * This function contains the logic for creating a payment. See {@link PaymentMethodHandler} for an example. * + * Returns a {@link CreatePaymentResult}. + * * @docsCategory payment * @docsPage Payment Method Types */ @@ -177,6 +199,9 @@ export interface PaymentMethodConfigOptions extends Config * third-party payment gateway before the Payment is created and can also define actions to fire * when the state of the payment is changed. * + * PaymentMethodHandlers are instantiated using a {@link PaymentMethodConfigOptions} object, which + * configures the business logic used to create, settle and refund payments. + * * @example * ```ts * import { PaymentMethodHandler, CreatePaymentResult, SettlePaymentResult, LanguageCode } from '\@vendure/core'; diff --git a/packages/core/src/entity/payment-method/payment-method.entity.ts b/packages/core/src/entity/payment-method/payment-method.entity.ts index 56b577f81b..41679eb98c 100644 --- a/packages/core/src/entity/payment-method/payment-method.entity.ts +++ b/packages/core/src/entity/payment-method/payment-method.entity.ts @@ -2,11 +2,7 @@ import { ConfigArg } from '@vendure/common/lib/generated-types'; import { DeepPartial } from '@vendure/common/lib/shared-types'; import { Column, Entity } from 'typeorm'; -import { UserInputError } from '../../common/error/errors'; -import { getConfig } from '../../config/config-helpers'; import { VendureEntity } from '../base/base.entity'; -import { Order } from '../order/order.entity'; -import { Payment, PaymentMetadata } from '../payment/payment.entity'; /** * @description diff --git a/packages/core/src/entity/payment/payment.entity.ts b/packages/core/src/entity/payment/payment.entity.ts index b2cf5afe47..f4d12668ee 100644 --- a/packages/core/src/entity/payment/payment.entity.ts +++ b/packages/core/src/entity/payment/payment.entity.ts @@ -1,13 +1,12 @@ import { DeepPartial } from '@vendure/common/lib/shared-types'; import { Column, Entity, ManyToOne, OneToMany } from 'typeorm'; +import { PaymentMetadata } from '../../common/types/common-types'; import { PaymentState } from '../../service/helpers/payment-state-machine/payment-state'; import { VendureEntity } from '../base/base.entity'; import { Order } from '../order/order.entity'; import { Refund } from '../refund/refund.entity'; -export type PaymentMetadata = any; - /** * @description * A Payment represents a single payment transaction and exists in a well-defined state @@ -27,8 +26,8 @@ export class Payment extends VendureEntity { @Column('varchar') state: PaymentState; - @Column({ nullable: true }) - errorMessage: string; + @Column({ type: 'varchar', nullable: true }) + errorMessage: string | undefined; @Column({ nullable: true }) transactionId: string; diff --git a/packages/core/src/entity/refund/refund.entity.ts b/packages/core/src/entity/refund/refund.entity.ts index 52fb55f9d7..4043d1bd26 100644 --- a/packages/core/src/entity/refund/refund.entity.ts +++ b/packages/core/src/entity/refund/refund.entity.ts @@ -1,11 +1,12 @@ import { DeepPartial, ID } from '@vendure/common/lib/shared-types'; import { Column, Entity, JoinColumn, JoinTable, ManyToOne, OneToMany } from 'typeorm'; +import { PaymentMetadata } from '../../common/types/common-types'; import { RefundState } from '../../service/helpers/refund-state-machine/refund-state'; import { VendureEntity } from '../base/base.entity'; import { EntityId } from '../entity-id.decorator'; import { OrderItem } from '../order-item/order-item.entity'; -import { Payment, PaymentMetadata } from '../payment/payment.entity'; +import { Payment } from '../payment/payment.entity'; @Entity() export class Refund extends VendureEntity { diff --git a/packages/core/src/service/services/order.service.ts b/packages/core/src/service/services/order.service.ts index fc66cd8730..63539b081d 100644 --- a/packages/core/src/service/services/order.service.ts +++ b/packages/core/src/service/services/order.service.ts @@ -60,7 +60,7 @@ import { PaymentDeclinedError, PaymentFailedError, } from '../../common/error/generated-graphql-shop-errors'; -import { ListQueryOptions } from '../../common/types/common-types'; +import { ListQueryOptions, PaymentMetadata } from '../../common/types/common-types'; import { assertFound, idsAreEqual } from '../../common/utils'; import { ConfigService } from '../../config/config.service'; import { Customer } from '../../entity/customer/customer.entity'; @@ -609,10 +609,10 @@ export class OrderService { await this.connection.getRepository(ctx, Order).save(order, { reload: false }); if (payment.state === 'Error') { - return new PaymentFailedError(payment.errorMessage); + return new PaymentFailedError(payment.errorMessage || ''); } if (payment.state === 'Declined') { - return new PaymentDeclinedError(payment.errorMessage); + return new PaymentDeclinedError(payment.errorMessage || ''); } if (orderTotalIsCovered(order, 'Settled')) { @@ -645,7 +645,7 @@ export class OrderService { const transitionError = ctx.translate(e.message, { fromState, toState }); return new PaymentStateTransitionError(transitionError, fromState, toState); } - payment.metadata = { ...payment.metadata, ...settlePaymentResult.metadata }; + payment.metadata = this.mergePaymentMetadata(payment.metadata, settlePaymentResult.metadata); await this.connection.getRepository(ctx, Payment).save(payment, { reload: false }); this.eventBus.publish( new PaymentStateTransitionEvent(fromState, toState, ctx, payment, payment.order), @@ -665,6 +665,9 @@ export class OrderService { } } } else { + payment.errorMessage = settlePaymentResult.errorMessage; + payment.metadata = this.mergePaymentMetadata(payment.metadata, settlePaymentResult.metadata); + await this.connection.getRepository(ctx, Payment).save(payment, { reload: false }); return new SettlePaymentError(settlePaymentResult.errorMessage || ''); } return payment; @@ -1107,4 +1110,15 @@ export class OrderService { items: Array.from(items.values()), }; } + + private mergePaymentMetadata(m1: PaymentMetadata, m2?: PaymentMetadata): PaymentMetadata { + if (!m2) { + return m1; + } + const merged = { ...m1, ...m2 }; + if (m1.public && m1.public) { + merged.public = { ...m1.public, ...m2.public }; + } + return merged; + } } diff --git a/packages/core/src/service/services/payment-method.service.ts b/packages/core/src/service/services/payment-method.service.ts index d95210a238..f78e6631bc 100644 --- a/packages/core/src/service/services/payment-method.service.ts +++ b/packages/core/src/service/services/payment-method.service.ts @@ -18,7 +18,7 @@ import { PaymentMethodHandler } from '../../config/payment-method/payment-method import { OrderItem } from '../../entity/order-item/order-item.entity'; import { Order } from '../../entity/order/order.entity'; import { PaymentMethod } from '../../entity/payment-method/payment-method.entity'; -import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity'; +import { Payment } from '../../entity/payment/payment.entity'; import { Refund } from '../../entity/refund/refund.entity'; import { EventBus } from '../../event-bus/event-bus'; import { PaymentStateTransitionEvent } from '../../event-bus/events/payment-state-transition-event'; @@ -78,12 +78,7 @@ export class PaymentMethodService { return this.connection.getRepository(ctx, PaymentMethod).save(updatedPaymentMethod); } - async createPayment( - ctx: RequestContext, - order: Order, - method: string, - metadata: PaymentMetadata, - ): Promise { + async createPayment(ctx: RequestContext, order: Order, method: string, metadata: any): Promise { const { paymentMethod, handler } = await this.getMethodAndHandler(ctx, method); const result = await handler.createPayment(order, paymentMethod.configArgs, metadata || {}); const initialState = 'Created';