From ad0c68ca5d3c5c3696547b0873181f9ac8d11aeb Mon Sep 17 00:00:00 2001 From: Nick Evans <2616208+nickevansuk@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:20:19 +0100 Subject: [PATCH] feat: Idempotency support (#552) --- .../test/features/README.md | 4 +- .../implemented/accept-proposal-book-test.js | 1 + .../customer-reject-proposal-test.js | 3 ++ .../not-accept-proposal-book-test.js | 3 ++ .../seller-reject-proposal-test.js | 3 ++ .../proposal-amendment-book-test.js | 4 ++ .../implemented/amend-c1-and-c2-test.js | 1 + .../implemented/amend-c1-test.js | 1 + .../implemented/amend-c2-test.js | 1 + .../c2-with-different-details-test.js | 1 + .../test/features/criteria-requirements.json | 6 +-- .../payment/free-opportunities/README.md | 3 +- .../opportunity-free-idempotency-test.js | 33 +++++++++++++++ ...portunity-free-without-checkpoints-test.js | 29 +++++++------ .../payment/non-free-opportunities/README.md | 3 +- .../opportunity-paid-idempotency-test.js | 36 ++++++++++++++++ ...portunity-paid-without-checkpoints-test.js | 29 +++++++------ ...culation-gross-without-checkpoints-test.js | 29 +++++++------ .../test/features/tests-implemented.json | 4 +- .../test/helpers/flow-stages/b.js | 36 +++++++++++++++- .../helpers/flow-stages/flow-stage-recipes.js | 41 +++++++++++++++---- .../test/helpers/flow-stages/p.js | 23 ++++++++++- .../test/templates/b-req.js | 7 ++-- .../test/templates/c1-req.js | 4 +- .../test/templates/c2-req.js | 4 +- .../test/templates/common.js | 12 +++--- 26 files changed, 251 insertions(+), 70 deletions(-) create mode 100644 packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-idempotency-test.js create mode 100644 packages/openactive-integration-tests/test/features/payment/non-free-opportunities/implemented/opportunity-paid-idempotency-test.js diff --git a/packages/openactive-integration-tests/test/features/README.md b/packages/openactive-integration-tests/test/features/README.md index f70c85f0ec..0f17792a14 100644 --- a/packages/openactive-integration-tests/test/features/README.md +++ b/packages/openactive-integration-tests/test/features/README.md @@ -47,8 +47,8 @@ The tests for these features cover all known edge cases, including both happy an | notifications | Change of logistics notifications ([change-of-logistics-notifications](./notifications/change-of-logistics-notifications/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#change-of-logistics-notifications) | Notifications for when an opportunity's name, location, or start/end date/time are updated | [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x12 | | notifications | Customer notice notifications ([customer-notice-notifications](./notifications/customer-notice-notifications/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#customer-notice-notifications) | Text notifications broadcast to all registered attendees of an opportunity | [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x4 | | notifications | Opportunity attendance updates ([opportunity-attendance-updates](./notifications/opportunity-attendance-updates/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#opportunity-attendance-updates) | Allowing the broker to recieve updates for when an attendee attends an event | [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x8 | -| payment | Free opportunities ([free-opportunities](./payment/free-opportunities/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#free-opportunities) | The most simple form of booking, for free opportunities. Does not check for leases. | [TestOpportunityBookableFree](https://openactive.io/test-interface#TestOpportunityBookableFree) x10 | -| payment | Opportunities with a non-zero price ([non-free-opportunities](./payment/non-free-opportunities/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#step-by-step-process-description) | The most simple form of booking with payment. Does not check for leases. | [TestOpportunityBookableNonFree](https://openactive.io/test-interface#TestOpportunityBookableNonFree) x6, [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x2 | +| payment | Free opportunities ([free-opportunities](./payment/free-opportunities/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#free-opportunities) | The most simple form of booking, for free opportunities. Does not check for leases. | [TestOpportunityBookableFree](https://openactive.io/test-interface#TestOpportunityBookableFree) x14 | +| payment | Opportunities with a non-zero price ([non-free-opportunities](./payment/non-free-opportunities/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#step-by-step-process-description) | The most simple form of booking with payment. Does not check for leases. | [TestOpportunityBookableNonFree](https://openactive.io/test-interface#TestOpportunityBookableNonFree) x9, [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x3 | | payment | Payment reconciliation detail validation ([payment-reconciliation-detail-validation](./payment/payment-reconciliation-detail-validation/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#payment-reconciliation-detail-validation) | Booking with valid, invalid, and missing Payment details | [TestOpportunityBookableFree](https://openactive.io/test-interface#TestOpportunityBookableFree) x12, [TestOpportunityBookableUsingPayment](https://openactive.io/test-interface#TestOpportunityBookableUsingPayment) x15, [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x5 | | payment | prepayment optional ([prepayment-optional](./payment/prepayment-optional/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#booking-without-payment) | Support for booking with optional payment | [TestOpportunityBookableNonFreePrepaymentOptional](https://openactive.io/test-interface#TestOpportunityBookableNonFreePrepaymentOptional) x8 | | payment | prepayment required ([prepayment-required](./payment/prepayment-required/README.md)) | Optional
[View Spec](https://www.openactive.io/open-booking-api/EditorsDraft/#booking-without-payment) | Support for booking with required payment | [TestOpportunityBookableNonFreePrepaymentRequired](https://openactive.io/test-interface#TestOpportunityBookableNonFreePrepaymentRequired) x8 | diff --git a/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/accept-proposal-book-test.js b/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/accept-proposal-book-test.js index 87466f80a5..297ee52186 100644 --- a/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/accept-proposal-book-test.js +++ b/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/accept-proposal-book-test.js @@ -56,6 +56,7 @@ FeatureHelper.describeFeature(module, { opportunityFeedExtractResponses: c2.getStage('assertOpportunityCapacityAfterC2').getOutput().opportunityFeedExtractResponses, orderItems: fetchOpportunities.getOutput().orderItems, }), + paymentIdentifierIfPaid: FlowStageRecipes.createRandomPaymentIdentifierIfPaid(), }); // ## Set up tests diff --git a/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/customer-reject-proposal-test.js b/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/customer-reject-proposal-test.js index 4f67f6508d..302ad6fb62 100644 --- a/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/customer-reject-proposal-test.js +++ b/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/customer-reject-proposal-test.js @@ -40,6 +40,7 @@ FeatureHelper.describeFeature(module, { }, (configuration, orderItemCriteriaList, featureIsImplemented, logger) => { const { fetchOpportunities, c1, c2, defaultFlowStageParams } = FlowStageRecipes.initialiseSimpleC1C2Flow(orderItemCriteriaList, logger); + const paymentIdentifierIfPaid = FlowStageRecipes.createRandomPaymentIdentifierIfPaid(); const p = new PFlowStage({ ...defaultFlowStageParams, prerequisite: c2.getLastStage(), @@ -48,6 +49,7 @@ FeatureHelper.describeFeature(module, { totalPaymentDue: c2.getStage('c2').getOutput().totalPaymentDue, prepayment: c2.getStage('c2').getOutput().prepayment, }), + paymentIdentifierIfPaid, }); const [customerRejection, orderFeedUpdate] = OrderFeedUpdateFlowStageUtils.wrap({ wrappedStageFn: prerequisite => (new CustomerRejectOrderProposalFlowStage({ @@ -73,6 +75,7 @@ FeatureHelper.describeFeature(module, { orderProposalVersion: orderFeedUpdate.getOutput().orderProposalVersion, positionOrderIntakeFormMap: c1.getStage('c1').getOutput().positionOrderIntakeFormMap, }), + paymentIdentifierIfPaid, }, }); diff --git a/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/not-accept-proposal-book-test.js b/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/not-accept-proposal-book-test.js index 283c13333a..3de1205417 100644 --- a/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/not-accept-proposal-book-test.js +++ b/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/not-accept-proposal-book-test.js @@ -34,6 +34,7 @@ FeatureHelper.describeFeature(module, { }, (configuration, orderItemCriteriaList, featureIsImplemented, logger) => { const { fetchOpportunities, c1, c2, defaultFlowStageParams } = FlowStageRecipes.initialiseSimpleC1C2Flow(orderItemCriteriaList, logger); + const paymentIdentifierIfPaid = FlowStageRecipes.createRandomPaymentIdentifierIfPaid(); const p = new PFlowStage({ ...defaultFlowStageParams, prerequisite: c2.getLastStage(), @@ -42,6 +43,7 @@ FeatureHelper.describeFeature(module, { totalPaymentDue: c2.getStage('c2').getOutput().totalPaymentDue, prepayment: c2.getStage('c2').getOutput().prepayment, }), + paymentIdentifierIfPaid, }); const b = FlowStageRecipes.runs.book.simpleBAssertCapacity(p, defaultFlowStageParams, { isExpectedToSucceed: false, @@ -55,6 +57,7 @@ FeatureHelper.describeFeature(module, { orderProposalVersion: p.getOutput().orderProposalVersion, positionOrderIntakeFormMap: c1.getStage('c1').getOutput().positionOrderIntakeFormMap, }), + paymentIdentifierIfPaid, }, }); diff --git a/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/seller-reject-proposal-test.js b/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/seller-reject-proposal-test.js index cef0033ea4..d4537692c2 100644 --- a/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/seller-reject-proposal-test.js +++ b/packages/openactive-integration-tests/test/features/approval/minimal-proposal/implemented/seller-reject-proposal-test.js @@ -40,6 +40,7 @@ FeatureHelper.describeFeature(module, { }, (configuration, orderItemCriteriaList, featureIsImplemented, logger) => { const { fetchOpportunities, c1, c2, defaultFlowStageParams } = FlowStageRecipes.initialiseSimpleC1C2Flow(orderItemCriteriaList, logger); + const paymentIdentifierIfPaid = FlowStageRecipes.createRandomPaymentIdentifierIfPaid(); const p = new PFlowStage({ ...defaultFlowStageParams, prerequisite: c2.getLastStage(), @@ -48,6 +49,7 @@ FeatureHelper.describeFeature(module, { totalPaymentDue: c2.getStage('c2').getOutput().totalPaymentDue, prepayment: c2.getStage('c2').getOutput().prepayment, }), + paymentIdentifierIfPaid, }); const [simulateSellerRejection, orderFeedUpdate] = OrderFeedUpdateFlowStageUtils.wrap({ wrappedStageFn: prerequisite => (new TestInterfaceActionFlowStage({ @@ -79,6 +81,7 @@ FeatureHelper.describeFeature(module, { orderProposalVersion: orderFeedUpdate.getOutput().orderProposalVersion, positionOrderIntakeFormMap: c1.getStage('c1').getOutput().positionOrderIntakeFormMap, }), + paymentIdentifierIfPaid, }, }); diff --git a/packages/openactive-integration-tests/test/features/approval/proposal-amendment/implemented/proposal-amendment-book-test.js b/packages/openactive-integration-tests/test/features/approval/proposal-amendment/implemented/proposal-amendment-book-test.js index ce7e688c49..1455ef1d4c 100644 --- a/packages/openactive-integration-tests/test/features/approval/proposal-amendment/implemented/proposal-amendment-book-test.js +++ b/packages/openactive-integration-tests/test/features/approval/proposal-amendment/implemented/proposal-amendment-book-test.js @@ -41,6 +41,7 @@ FeatureHelper.describeFeature(module, { (configuration, orderItemCriteriaList, featureIsImplemented, logger) => { // ## Initiate Flow Stages const { fetchOpportunities, c1, c2, defaultFlowStageParams } = FlowStageRecipes.initialiseSimpleC1C2Flow(orderItemCriteriaList, logger); + const paymentIdentifierIfPaid = FlowStageRecipes.createRandomPaymentIdentifierIfPaid(); const p = new PFlowStage({ ...defaultFlowStageParams, prerequisite: c2.getLastStage(), @@ -49,6 +50,7 @@ FeatureHelper.describeFeature(module, { totalPaymentDue: c2.getStage('c2').getOutput().totalPaymentDue, prepayment: c2.getStage('c2').getOutput().prepayment, }), + paymentIdentifierIfPaid, }); const [simulateSellerAmendment, sellerAmendmentOrderFeedUpdate] = OrderFeedUpdateFlowStageUtils.wrap({ // FlowStage that is getting wrapped @@ -102,6 +104,7 @@ FeatureHelper.describeFeature(module, { orderProposalVersion: p.getOutput().orderProposalVersion, prepayment: p.getOutput().prepayment, }), + paymentIdentifierIfPaid, }, }); // Using the new proposal version should fail @@ -116,6 +119,7 @@ FeatureHelper.describeFeature(module, { orderProposalVersion: sellerAmendmentOrderFeedUpdate.getOutput().orderProposalVersion, prepayment: sellerAmendmentOrderFeedUpdate.getOutput().prepayment, }), + paymentIdentifierIfPaid, }, }); diff --git a/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c1-and-c2-test.js b/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c1-and-c2-test.js index 5b0d082d82..4a73d17e29 100644 --- a/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c1-and-c2-test.js +++ b/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c1-and-c2-test.js @@ -77,6 +77,7 @@ FeatureHelper.describeFeature(module, { orderItems: secondAttemptFetchOpportunities.getOutput().orderItems, opportunityFeedExtractResponses: secondAttemptC2.getStage('assertOpportunityCapacityAfterC2').getOutput().opportunityFeedExtractResponses, }), + paymentIdentifierIfPaid: FlowStageRecipes.createRandomPaymentIdentifierIfPaid(), }); // # Set up Tests diff --git a/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c1-test.js b/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c1-test.js index d4614c3fec..ef77bc20cc 100644 --- a/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c1-test.js +++ b/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c1-test.js @@ -74,6 +74,7 @@ FeatureHelper.describeFeature(module, { orderItems: secondAttemptFetchOpportunities.getOutput().orderItems, opportunityFeedExtractResponses: secondAttemptC2.getStage('assertOpportunityCapacityAfterC2').getOutput().opportunityFeedExtractResponses, }), + paymentIdentifierIfPaid: FlowStageRecipes.createRandomPaymentIdentifierIfPaid(), }); // # Set up Tests diff --git a/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c2-test.js b/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c2-test.js index 084754f002..87b88b6ccd 100644 --- a/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c2-test.js +++ b/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/amend-c2-test.js @@ -69,6 +69,7 @@ FeatureHelper.describeFeature(module, { orderItems: secondAttemptFetchOpportunities.getOutput().orderItems, opportunityFeedExtractResponses: secondAttemptC2.getStage('assertOpportunityCapacityAfterC2').getOutput().opportunityFeedExtractResponses, }), + paymentIdentifierIfPaid: FlowStageRecipes.createRandomPaymentIdentifierIfPaid(), }); // # Set up Tests diff --git a/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/c2-with-different-details-test.js b/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/c2-with-different-details-test.js index 1923d3a73e..b9387e2829 100644 --- a/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/c2-with-different-details-test.js +++ b/packages/openactive-integration-tests/test/features/core/amending-order-quote/implemented/c2-with-different-details-test.js @@ -60,6 +60,7 @@ FeatureHelper.describeFeature(module, { orderItems: secondAttemptFetchOpportunities.getOutput().orderItems, opportunityFeedExtractResponses: secondAttemptC2.getStage('assertOpportunityCapacityAfterC2').getOutput().opportunityFeedExtractResponses, }), + paymentIdentifierIfPaid: FlowStageRecipes.createRandomPaymentIdentifierIfPaid(), }); // # Set up Tests diff --git a/packages/openactive-integration-tests/test/features/criteria-requirements.json b/packages/openactive-integration-tests/test/features/criteria-requirements.json index db704d5d7b..dbd7fb0534 100644 --- a/packages/openactive-integration-tests/test/features/criteria-requirements.json +++ b/packages/openactive-integration-tests/test/features/criteria-requirements.json @@ -194,13 +194,13 @@ }, "free-opportunities": { "primary": { - "TestOpportunityBookableFree": 10 + "TestOpportunityBookableFree": 14 } }, "non-free-opportunities": { "primary": { - "TestOpportunityBookableNonFree": 6, - "TestOpportunityBookable": 2 + "TestOpportunityBookableNonFree": 9, + "TestOpportunityBookable": 3 } }, "payment-reconciliation-detail-validation": { diff --git a/packages/openactive-integration-tests/test/features/payment/free-opportunities/README.md b/packages/openactive-integration-tests/test/features/payment/free-opportunities/README.md index 8ba9ed0c87..cd263923cd 100644 --- a/packages/openactive-integration-tests/test/features/payment/free-opportunities/README.md +++ b/packages/openactive-integration-tests/test/features/payment/free-opportunities/README.md @@ -12,7 +12,7 @@ See also: [.NET Tutorial](https://tutorials.openactive.io/open-booking-sdk/quick ### Test prerequisites Opportunities that match the following criteria must exist in the booking system (for each configured `bookableOpportunityTypesInScope`) for the configured primary Seller in order to use `useRandomOpportunities: true`. Alternatively the following `testOpportunityCriteria` values must be supported by the [test interface](https://openactive.io/test-interface/) of the booking system for `useRandomOpportunities: false`. -[TestOpportunityBookableFree](https://openactive.io/test-interface#TestOpportunityBookableFree) x10 +[TestOpportunityBookableFree](https://openactive.io/test-interface#TestOpportunityBookableFree) x14 ### Running tests for only this feature @@ -37,6 +37,7 @@ Update `default.json` within `packages/openactive-integration-tests/config/` as | Identifier | Name | Description | Prerequisites per Opportunity Type | |------------|------|-------------|---------------| +| [opportunity-free-idempotency](./implemented/opportunity-free-idempotency-test.js) | Successful booking of free opportunity with idempotency | Testing idempotency of the B call for free opportunities | [TestOpportunityBookableFree](https://openactive.io/test-interface#TestOpportunityBookableFree) x4 | | [opportunity-free-must-not-include-prepayment](./implemented/opportunity-free-must-not-include-prepayment-test.js) | Free opportunities must have either a `openBookingPrepayment` value of Unspecified, or have no `openBookingPrepayment` specified | Assert that no opportunities that match criteria 'TestOpportunityBookableFreePrepaymentOptional' or 'TestOpportunityBookableFreePrepaymentRequired' are available in the opportunity feeds. | | | [opportunity-free](./implemented/opportunity-free-test.js) | Successful booking without payment property | A successful end to end booking without the `payment` property included. | [TestOpportunityBookableFree](https://openactive.io/test-interface#TestOpportunityBookableFree) x2 | | [opportunity-free-unnecessary-payment-error](./implemented/opportunity-free-unnecessary-payment-error-test.js) | Fail free bookings which include erroneous payment property | C1, C2 and B with payment property: payment property is provided but not expected in the request, so an UnnecessaryPaymentDetailsError must be returned. | [TestOpportunityBookableFree](https://openactive.io/test-interface#TestOpportunityBookableFree) x2 | diff --git a/packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-idempotency-test.js b/packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-idempotency-test.js new file mode 100644 index 0000000000..af2c9f1f7b --- /dev/null +++ b/packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-idempotency-test.js @@ -0,0 +1,33 @@ +const { omit } = require('lodash'); +const { FeatureHelper } = require('../../../../helpers/feature-helper'); +const { FlowStageRecipes, FlowStageUtils } = require('../../../../helpers/flow-stages'); + +FeatureHelper.describeFeature(module, { + testCategory: 'payment', + testFeature: 'free-opportunities', + testFeatureImplemented: true, + testIdentifier: 'opportunity-free-idempotency', + testName: 'Successful booking of free opportunity with idempotency', + testDescription: 'Testing idempotency of the B call for free opportunities', + testOpportunityCriteria: 'TestOpportunityBookableFree', + // This must also be TestOpportunityBookableFree as the entire Order must be free. + controlOpportunityCriteria: 'TestOpportunityBookableFree', +}, (configuration, orderItemCriteriaList, featureIsImplemented, logger) => { + const { + fetchOpportunities, + bookRecipe, + defaultFlowStageParams, + bookRecipeArgs, + } = FlowStageRecipes.initialiseSimpleC1C2BookFlow(orderItemCriteriaList, logger); + const idempotentRepeatB = FlowStageRecipes.idempotentRepeatBAfterBook( + orderItemCriteriaList, + bookRecipe, + defaultFlowStageParams, + omit(bookRecipeArgs, ['prerequisite']), + ); + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(fetchOpportunities); + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(bookRecipe); + describe('idempotent repeat B', () => { + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(idempotentRepeatB); + }); +}); diff --git a/packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-without-checkpoints-test.js b/packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-without-checkpoints-test.js index 0128e79922..85607dd2a3 100644 --- a/packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-without-checkpoints-test.js +++ b/packages/openactive-integration-tests/test/features/payment/free-opportunities/implemented/opportunity-free-without-checkpoints-test.js @@ -1,3 +1,4 @@ +const { omit } = require('lodash'); const { FeatureHelper } = require('../../../../helpers/feature-helper'); const { FlowStageRecipes, FlowStageUtils } = require('../../../../helpers/flow-stages'); @@ -12,18 +13,22 @@ FeatureHelper.describeFeature(module, { testOpportunityCriteria: 'TestOpportunityBookableFree', // This must also be TestOpportunityBookableFree as the entire Order must be free. controlOpportunityCriteria: 'TestOpportunityBookableFree', -}, (configuration, orderItemCriteriaList, featureIsImplemented, logger, opportunityType, bookingFlow) => { - const { fetchOpportunities, bookRecipe, defaultFlowStageParams, bookRecipeGetFirstStageInput, bookRecipeGetAssertOpportunityCapacityInput } = FlowStageRecipes.initialiseSimpleBookOnlyFlow(orderItemCriteriaList, logger); - const idempotentRepeatB = FlowStageRecipes.idempotentRepeatBAfterBook(orderItemCriteriaList, bookRecipe, defaultFlowStageParams, { - getFirstStageInput: bookRecipeGetFirstStageInput, - getAssertOpportunityCapacityInput: bookRecipeGetAssertOpportunityCapacityInput, - }); +}, (configuration, orderItemCriteriaList, featureIsImplemented, logger) => { + const { + fetchOpportunities, + bookRecipe, + defaultFlowStageParams, + bookRecipeArgs, + } = FlowStageRecipes.initialiseSimpleBookOnlyFlow(orderItemCriteriaList, logger); + const idempotentRepeatB = FlowStageRecipes.idempotentRepeatBAfterBook( + orderItemCriteriaList, + bookRecipe, + defaultFlowStageParams, + omit(bookRecipeArgs, ['prerequisite']), + ); FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(fetchOpportunities); FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(bookRecipe); - // Remove this condition once https://github.com/openactive/OpenActive.Server.NET/issues/100 is fixed. - if (bookingFlow === 'OpenBookingApprovalFlow') { - describe('idempotent repeat B', () => { - FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(idempotentRepeatB); - }); - } + describe('idempotent repeat B', () => { + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(idempotentRepeatB); + }); }); diff --git a/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/README.md b/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/README.md index d8333661d5..82623512c3 100644 --- a/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/README.md +++ b/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/README.md @@ -12,7 +12,7 @@ See also: [.NET Tutorial](https://tutorials.openactive.io/open-booking-sdk/quick ### Test prerequisites Opportunities that match the following criteria must exist in the booking system (for each configured `bookableOpportunityTypesInScope`) for the configured primary Seller in order to use `useRandomOpportunities: true`. Alternatively the following `testOpportunityCriteria` values must be supported by the [test interface](https://openactive.io/test-interface/) of the booking system for `useRandomOpportunities: false`. -[TestOpportunityBookableNonFree](https://openactive.io/test-interface#TestOpportunityBookableNonFree) x6, [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x2 +[TestOpportunityBookableNonFree](https://openactive.io/test-interface#TestOpportunityBookableNonFree) x9, [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x3 ### Running tests for only this feature @@ -37,6 +37,7 @@ Update `default.json` within `packages/openactive-integration-tests/config/` as | Identifier | Name | Description | Prerequisites per Opportunity Type | |------------|------|-------------|---------------| +| [opportunity-paid-idempotency](./implemented/opportunity-paid-idempotency-test.js) | Successful booking of paid opportunity with idempotency | Testing idempotency of the B call for paid opportunities | [TestOpportunityBookableNonFree](https://openactive.io/test-interface#TestOpportunityBookableNonFree) x3, [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x1 | | [opportunity-paid](./implemented/opportunity-paid-test.js) | Successful booking with payment property | A successful end to end booking of a non-free opportunity with the `payment` property included if required. | [TestOpportunityBookableNonFree](https://openactive.io/test-interface#TestOpportunityBookableNonFree) x3, [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x1 | | [opportunity-paid-without-checkpoints](./implemented/opportunity-paid-without-checkpoints-test.js) | Successful booking without Checkpoints | Paid Opportunities should be bookable without using Checkpoints C1 & C2 if 1). tax calculations are not performed by the Booking System and 2). they do not require additional details | [TestOpportunityBookableNonFree](https://openactive.io/test-interface#TestOpportunityBookableNonFree) x3, [TestOpportunityBookable](https://openactive.io/test-interface#TestOpportunityBookable) x1 | diff --git a/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/implemented/opportunity-paid-idempotency-test.js b/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/implemented/opportunity-paid-idempotency-test.js new file mode 100644 index 0000000000..b764589220 --- /dev/null +++ b/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/implemented/opportunity-paid-idempotency-test.js @@ -0,0 +1,36 @@ +const { omit } = require('lodash'); +const { FeatureHelper } = require('../../../../helpers/feature-helper'); +const { FlowStageRecipes, FlowStageUtils } = require('../../../../helpers/flow-stages'); + +FeatureHelper.describeFeature(module, { + testCategory: 'payment', + testFeature: 'non-free-opportunities', + testFeatureImplemented: true, + testIdentifier: 'opportunity-paid-idempotency', + testName: 'Successful booking of paid opportunity with idempotency', + testDescription: 'Testing idempotency of the B call for paid opportunities', + testOpportunityCriteria: 'TestOpportunityBookableNonFree', + controlOpportunityCriteria: 'TestOpportunityBookable', +}, +(configuration, orderItemCriteriaList, featureIsImplemented, logger) => { + // Initiate Flow Stages + const { + fetchOpportunities, + bookRecipe, + defaultFlowStageParams, + bookRecipeArgs, + } = FlowStageRecipes.initialiseSimpleC1C2BookFlow(orderItemCriteriaList, logger); + const idempotentRepeatB = FlowStageRecipes.idempotentRepeatBAfterBook( + orderItemCriteriaList, + bookRecipe, + defaultFlowStageParams, + omit(bookRecipeArgs, ['prerequisite']), + ); + + // Set up tests + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(fetchOpportunities); + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(bookRecipe); + describe('idempotent repeat B', () => { + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(idempotentRepeatB); + }); +}); diff --git a/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/implemented/opportunity-paid-without-checkpoints-test.js b/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/implemented/opportunity-paid-without-checkpoints-test.js index 98637eaf35..cc7f063461 100644 --- a/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/implemented/opportunity-paid-without-checkpoints-test.js +++ b/packages/openactive-integration-tests/test/features/payment/non-free-opportunities/implemented/opportunity-paid-without-checkpoints-test.js @@ -1,3 +1,4 @@ +const { omit } = require('lodash'); const { FeatureHelper } = require('../../../../helpers/feature-helper'); const { FlowStageRecipes, FlowStageUtils } = require('../../../../helpers/flow-stages'); @@ -19,21 +20,25 @@ FeatureHelper.describeFeature(module, { runOnlyIf: !IMPLEMENTED_FEATURES['business-to-consumer-tax-calculation-gross'] && !IMPLEMENTED_FEATURES['business-to-consumer-tax-calculation-net'], }, -(configuration, orderItemCriteriaList, featureIsImplemented, logger, opportunityType, bookingFlow) => { +(configuration, orderItemCriteriaList, featureIsImplemented, logger) => { // Initiate Flow Stages - const { fetchOpportunities, bookRecipe, defaultFlowStageParams, bookRecipeGetFirstStageInput, bookRecipeGetAssertOpportunityCapacityInput } = FlowStageRecipes.initialiseSimpleBookOnlyFlow(orderItemCriteriaList, logger); - const idempotentRepeatB = FlowStageRecipes.idempotentRepeatBAfterBook(orderItemCriteriaList, bookRecipe, defaultFlowStageParams, { - getFirstStageInput: bookRecipeGetFirstStageInput, - getAssertOpportunityCapacityInput: bookRecipeGetAssertOpportunityCapacityInput, - }); + const { + fetchOpportunities, + bookRecipe, + defaultFlowStageParams, + bookRecipeArgs, + } = FlowStageRecipes.initialiseSimpleBookOnlyFlow(orderItemCriteriaList, logger); + const idempotentRepeatB = FlowStageRecipes.idempotentRepeatBAfterBook( + orderItemCriteriaList, + bookRecipe, + defaultFlowStageParams, + omit(bookRecipeArgs, ['prerequisite']), + ); // Set up tests FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(fetchOpportunities); FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(bookRecipe); - // Remove this condition once https://github.com/openactive/OpenActive.Server.NET/issues/100 is fixed. - if (bookingFlow === 'OpenBookingApprovalFlow') { - describe('idempotent repeat B', () => { - FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(idempotentRepeatB); - }); - } + describe('idempotent repeat B', () => { + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(idempotentRepeatB); + }); }); diff --git a/packages/openactive-integration-tests/test/features/tax/business-to-consumer-tax-calculation-gross/implemented/business-to-consumer-tax-calculation-gross-without-checkpoints-test.js b/packages/openactive-integration-tests/test/features/tax/business-to-consumer-tax-calculation-gross/implemented/business-to-consumer-tax-calculation-gross-without-checkpoints-test.js index 1b17f439de..a88ba4b7b2 100644 --- a/packages/openactive-integration-tests/test/features/tax/business-to-consumer-tax-calculation-gross/implemented/business-to-consumer-tax-calculation-gross-without-checkpoints-test.js +++ b/packages/openactive-integration-tests/test/features/tax/business-to-consumer-tax-calculation-gross/implemented/business-to-consumer-tax-calculation-gross-without-checkpoints-test.js @@ -1,3 +1,4 @@ +const { omit } = require('lodash'); const { FeatureHelper } = require('../../../../helpers/feature-helper'); const { FlowStageRecipes, FlowStageUtils } = require('../../../../helpers/flow-stages'); @@ -13,18 +14,22 @@ FeatureHelper.describeFeature(module, { testOpportunityCriteria: 'TestOpportunityBookableNonFreeTaxGross', // the simple tests can only work if all OrderItems have the same tax mode controlOpportunityCriteria: 'TestOpportunityBookableNonFreeTaxGross', -}, (configuration, orderItemCriteriaList, featureIsImplemented, logger, opportunityType, bookingFlow) => { - const { fetchOpportunities, bookRecipe, defaultFlowStageParams, bookRecipeGetFirstStageInput, bookRecipeGetAssertOpportunityCapacityInput } = FlowStageRecipes.initialiseSimpleBookOnlyFlow(orderItemCriteriaList, logger); - const idempotentRepeatB = FlowStageRecipes.idempotentRepeatBAfterBook(orderItemCriteriaList, bookRecipe, defaultFlowStageParams, { - getFirstStageInput: bookRecipeGetFirstStageInput, - getAssertOpportunityCapacityInput: bookRecipeGetAssertOpportunityCapacityInput, - }); +}, (configuration, orderItemCriteriaList, featureIsImplemented, logger) => { + const { + fetchOpportunities, + bookRecipe, + defaultFlowStageParams, + bookRecipeArgs, + } = FlowStageRecipes.initialiseSimpleBookOnlyFlow(orderItemCriteriaList, logger); + const idempotentRepeatB = FlowStageRecipes.idempotentRepeatBAfterBook( + orderItemCriteriaList, + bookRecipe, + defaultFlowStageParams, + omit(bookRecipeArgs, ['prerequisite']), + ); FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(fetchOpportunities); FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(bookRecipe); - // Remove this condition once https://github.com/openactive/OpenActive.Server.NET/issues/100 is fixed. - if (bookingFlow === 'OpenBookingApprovalFlow') { - describe('idempotent repeat B', () => { - FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(idempotentRepeatB); - }); - } + describe('idempotent repeat B', () => { + FlowStageUtils.describeRunAndCheckIsSuccessfulAndValid(idempotentRepeatB); + }); }); diff --git a/packages/openactive-integration-tests/test/features/tests-implemented.json b/packages/openactive-integration-tests/test/features/tests-implemented.json index 30d40853d8..fb06a6be46 100644 --- a/packages/openactive-integration-tests/test/features/tests-implemented.json +++ b/packages/openactive-integration-tests/test/features/tests-implemented.json @@ -162,11 +162,11 @@ "notImplementedTestFiles": 0 }, "free-opportunities": { - "implementedTestFiles": 5, + "implementedTestFiles": 6, "notImplementedTestFiles": 1 }, "non-free-opportunities": { - "implementedTestFiles": 2, + "implementedTestFiles": 3, "notImplementedTestFiles": 1 }, "payment-reconciliation-detail-validation": { diff --git a/packages/openactive-integration-tests/test/helpers/flow-stages/b.js b/packages/openactive-integration-tests/test/helpers/flow-stages/b.js index 5242867723..d7c8ba2c20 100644 --- a/packages/openactive-integration-tests/test/helpers/flow-stages/b.js +++ b/packages/openactive-integration-tests/test/helpers/flow-stages/b.js @@ -37,9 +37,24 @@ const { FlowStageUtils } = require('./flow-stage-utils'); * @param {RequestHelperType} args.requestHelper * @param {string | null} args.brokerRole * @param {PositionOrderIntakeFormMap} args.positionOrderIntakeFormMap + * @param {string} args.paymentIdentifierIfPaid * @returns {Promise} */ -async function runB({ templateRef, accessPass, brokerRole, uuid, sellerConfig, customer, orderItems, totalPaymentDue, prepayment, orderProposalVersion, requestHelper, positionOrderIntakeFormMap }) { +async function runB({ + templateRef, + accessPass, + brokerRole, + uuid, + sellerConfig, + customer, + orderItems, + totalPaymentDue, + prepayment, + orderProposalVersion, + requestHelper, + positionOrderIntakeFormMap, + paymentIdentifierIfPaid, +}) { /** @type {BReqTemplateData} */ const params = { orderType: 'Order', @@ -52,6 +67,7 @@ async function runB({ templateRef, accessPass, brokerRole, uuid, sellerConfig, c brokerRole, positionOrderIntakeFormMap, customer, + paymentIdentifier: paymentIdentifierIfPaid, }; const response = await requestHelper.putOrder(uuid, params, templateRef); const bookingSystemOrder = response.body; @@ -81,8 +97,23 @@ class BFlowStage extends FlowStage { * @param {string} args.uuid * @param {SellerConfig} args.sellerConfig * @param {Customer} [args.customer] + * @param {string} args.paymentIdentifierIfPaid This Payment Identifier will be used if this is a paid + * booking. Otherwise, it won't be. This is specified as an arg to allow for consistency between idempotent + * B calls. */ - constructor({ templateRef, accessPass, brokerRole, prerequisite, getInput, logger, requestHelper, uuid, sellerConfig, customer }) { + constructor({ + templateRef, + accessPass, + brokerRole, + prerequisite, + getInput, + logger, + requestHelper, + uuid, + sellerConfig, + customer, + paymentIdentifierIfPaid, + }) { super({ prerequisite, getInput, @@ -102,6 +133,7 @@ class BFlowStage extends FlowStage { orderProposalVersion, requestHelper, positionOrderIntakeFormMap, + paymentIdentifierIfPaid, }); }, itSuccessChecksFn: FlowStageUtils.simpleHttp201SuccessChecks(), diff --git a/packages/openactive-integration-tests/test/helpers/flow-stages/flow-stage-recipes.js b/packages/openactive-integration-tests/test/helpers/flow-stages/flow-stage-recipes.js index 335a4efc28..9dd6297dbb 100644 --- a/packages/openactive-integration-tests/test/helpers/flow-stages/flow-stage-recipes.js +++ b/packages/openactive-integration-tests/test/helpers/flow-stages/flow-stage-recipes.js @@ -1,3 +1,4 @@ +const shortid = require('shortid'); const { AssertOpportunityCapacityFlowStage } = require('./assert-opportunity-capacity'); const { BFlowStage } = require('./b'); const { BookRecipe } = require('./book-recipe'); @@ -63,6 +64,9 @@ const { TestInterfaceActionFlowStage } = require('./test-interface-action'); * AssertOpportunityCapacity flow stage, which runs after the booking is complete * @property {GetOpportunityExpectedCapacity} [getOpportunityExpectedCapacity] If not provided, this will * default to using getOpportunityExpectedCapacityAfterBook(..) + * @property {string} paymentIdentifierIfPaid This Payment Identifier will be used if this is a paid + * booking. Otherwise, it won't be. This is specified as an arg to allow for consistency between idempotent + * B/P calls. */ const RUN_TESTS_WHICH_FAIL_REFIMPL = process.env.RUN_TESTS_WHICH_FAIL_REFIMPL === 'true'; @@ -103,7 +107,8 @@ const FlowStageRecipes = { brokerRole, }, ); - const bookRecipe = FlowStageRecipes.book(orderItemCriteriaList, defaultFlowStageParams, { + /** @type {BookRecipeArgs} */ + const bookRecipeArgs = { prerequisite: c2.getLastStage(), accessPass, brokerRole, @@ -119,7 +124,9 @@ const FlowStageRecipes = { orderItems: fetchOpportunities.getOutput().orderItems, }), isExpectedToFail: bookExpectToFail, - }); + paymentIdentifierIfPaid: FlowStageRecipes.createRandomPaymentIdentifierIfPaid(), + }; + const bookRecipe = FlowStageRecipes.book(orderItemCriteriaList, defaultFlowStageParams, bookRecipeArgs); return { fetchOpportunities, @@ -129,6 +136,7 @@ const FlowStageRecipes = { // This is included in the result so that additional stages can be added using // these params. defaultFlowStageParams, + bookRecipeArgs, }; }, /** @@ -286,13 +294,20 @@ const FlowStageRecipes = { opportunityFeedExtractResponses: fetchOpportunities.getOutput().opportunityFeedExtractResponses, orderItems: fetchOpportunities.getOutput().orderItems, }); - const bookRecipe = FlowStageRecipes.book(orderItemCriteriaList, defaultFlowStageParams, { + const bookRecipeGetOpportunityExpectedCapacity = AssertOpportunityCapacityFlowStage.getOpportunityExpectedCapacityAfterBookOnly( + isExpectedToSucceed, + ); + const bookRecipePaymentIdentifierIfPaid = FlowStageRecipes.createRandomPaymentIdentifierIfPaid(); + /** @type {BookRecipeArgs} */ + const bookRecipeArgs = { prerequisite: fetchOpportunities, isExpectedToFail: !isExpectedToSucceed, getFirstStageInput: bookRecipeGetFirstStageInput, getAssertOpportunityCapacityInput: bookRecipeGetAssertOpportunityCapacityInput, - getOpportunityExpectedCapacity: AssertOpportunityCapacityFlowStage.getOpportunityExpectedCapacityAfterBookOnly(isExpectedToSucceed), - }); + getOpportunityExpectedCapacity: bookRecipeGetOpportunityExpectedCapacity, + paymentIdentifierIfPaid: bookRecipePaymentIdentifierIfPaid, + }; + const bookRecipe = FlowStageRecipes.book(orderItemCriteriaList, defaultFlowStageParams, bookRecipeArgs); return { fetchOpportunities, bookRecipe, @@ -300,8 +315,7 @@ const FlowStageRecipes = { // these params. defaultFlowStageParams, // These can be used to create an idempotent second B stage. - bookRecipeGetFirstStageInput, - bookRecipeGetAssertOpportunityCapacityInput, + bookRecipeArgs, }; }, /** @@ -367,6 +381,7 @@ const FlowStageRecipes = { bookRecipeGetFirstStageInput: bookRecipeArgs.getFirstStageInput, defaultFlowStageParams, prerequisite: bookRecipe.lastStage, + paymentIdentifierIfPaid: bookRecipeArgs.paymentIdentifierIfPaid, }); } const bookRecipeArgsWithPrerequisite = { ...bookRecipeArgs, prerequisite: bookRecipe.lastStage }; @@ -423,6 +438,7 @@ const FlowStageRecipes = { getFirstStageInput, getAssertOpportunityCapacityInput, isExpectedToFail = null, + paymentIdentifierIfPaid, ...args }) { const b = new BFlowStage({ @@ -432,6 +448,7 @@ const FlowStageRecipes = { brokerRole, accessPass, getInput: getFirstStageInput, + paymentIdentifierIfPaid, }); const getOpportunityExpectedCapacity = args.getOpportunityExpectedCapacity ?? AssertOpportunityCapacityFlowStage.getOpportunityExpectedCapacityAfterBook(!(isExpectedToFail ?? false)); @@ -467,6 +484,7 @@ const FlowStageRecipes = { getFirstStageInput, getAssertOpportunityCapacityInput, isExpectedToFail = null, + paymentIdentifierIfPaid, ...args }) { const p = new PFlowStage({ @@ -476,6 +494,7 @@ const FlowStageRecipes = { brokerRole, accessPass, getInput: getFirstStageInput, + paymentIdentifierIfPaid, }); const [simulateSellerApproval, orderFeedUpdateCollector] = OrderFeedUpdateFlowStageUtils.wrap({ wrappedStageFn: orderFeedUpdateListener => (new TestInterfaceActionFlowStage({ @@ -503,6 +522,7 @@ const FlowStageRecipes = { defaultFlowStageParams, prerequisite: orderFeedUpdateListener, bookRecipeGetFirstStageInput: getFirstStageInput, + paymentIdentifierIfPaid, }), orderFeedUpdateParams: { ...defaultFlowStageParams, @@ -766,6 +786,9 @@ const FlowStageRecipes = { }, }, }, + createRandomPaymentIdentifierIfPaid() { + return shortid.generate(); + }, }; /** @@ -776,8 +799,9 @@ const FlowStageRecipes = { * @param {DefaultFlowStageParams} args.defaultFlowStageParams * @param {UnknownFlowStageType} args.prerequisite * @param {() => import('./p').Input} args.bookRecipeGetFirstStageInput + * @param {string} args.paymentIdentifierIfPaid */ -function bAfterP({ p, defaultFlowStageParams, prerequisite, bookRecipeGetFirstStageInput }) { +function bAfterP({ p, defaultFlowStageParams, prerequisite, bookRecipeGetFirstStageInput, paymentIdentifierIfPaid }) { return new BFlowStage({ ...defaultFlowStageParams, prerequisite, @@ -793,6 +817,7 @@ function bAfterP({ p, defaultFlowStageParams, prerequisite, bookRecipeGetFirstSt prepayment: p.getOutput().prepayment, }; }, + paymentIdentifierIfPaid, }); } diff --git a/packages/openactive-integration-tests/test/helpers/flow-stages/p.js b/packages/openactive-integration-tests/test/helpers/flow-stages/p.js index 46222a157a..c933a0c30f 100644 --- a/packages/openactive-integration-tests/test/helpers/flow-stages/p.js +++ b/packages/openactive-integration-tests/test/helpers/flow-stages/p.js @@ -36,6 +36,7 @@ const { FlowStageUtils } = require('./flow-stage-utils'); * @param {PositionOrderIntakeFormMap} args.positionOrderIntakeFormMap * @param {AccessPassItem[] | null} [args.accessPass] * @param {string | null} args.brokerRole + * @param {string} args.paymentIdentifierIfPaid * @returns {Promise} */ async function runP({ @@ -50,8 +51,9 @@ async function runP({ accessPass, brokerRole, requestHelper, + paymentIdentifierIfPaid, }) { - /** @type {import('../../templates/b-req').BReqTemplateData} */ + /** @type {import('../../templates/b-req').PReqTemplateData} */ const params = { orderType: 'OrderProposal', sellerId: sellerConfig['@id'], @@ -62,6 +64,7 @@ async function runP({ brokerRole, positionOrderIntakeFormMap, customer, + paymentIdentifier: paymentIdentifierIfPaid, }; const response = await requestHelper.putOrderProposal(uuid, params, templateRef); const bookingSystemOrder = response.body; @@ -92,8 +95,23 @@ class PFlowStage extends FlowStage { * @param {string} args.uuid * @param {SellerConfig} args.sellerConfig * @param {Customer} [args.customer] + * @param {string} args.paymentIdentifierIfPaid This Payment Identifier will be used if this is a paid + * booking. Otherwise, it won't be. This is specified as an arg to allow for consistency between idempotent + * P calls. */ - constructor({ templateRef, accessPass, brokerRole, prerequisite, getInput, logger, requestHelper, uuid, sellerConfig, customer }) { + constructor({ + templateRef, + accessPass, + brokerRole, + prerequisite, + getInput, + logger, + requestHelper, + uuid, + sellerConfig, + customer, + paymentIdentifierIfPaid, + }) { super({ prerequisite, getInput, @@ -112,6 +130,7 @@ class PFlowStage extends FlowStage { prepayment, positionOrderIntakeFormMap, requestHelper, + paymentIdentifierIfPaid, }); }, itSuccessChecksFn: FlowStageUtils.simpleHttp201SuccessChecks(), diff --git a/packages/openactive-integration-tests/test/templates/b-req.js b/packages/openactive-integration-tests/test/templates/b-req.js index d067e501ae..60211e7682 100644 --- a/packages/openactive-integration-tests/test/templates/b-req.js +++ b/packages/openactive-integration-tests/test/templates/b-req.js @@ -43,6 +43,7 @@ const { createPaymentPart, isPaidOpportunity, isPaymentAvailable, addOrderItemIn * brokerRole?: string | null, * positionOrderIntakeFormMap: {[k:string]: import('../helpers/flow-stages/flow-stage').OrderItemIntakeForm}, * customer: import('../helpers/flow-stages/flow-stage-utils').Customer, + * paymentIdentifier?: string * }} BReqTemplateData * * @typedef {Omit} PReqTemplateData P accepts the same sort of requests as B. @@ -75,7 +76,7 @@ function createAfterPBReq(data) { orderProposalVersion: data.orderProposalVersion, }; if (isPaymentAvailable(data)) { - result.payment = createPaymentPart(); + result.payment = createPaymentPart(data.paymentIdentifier); } return result; } @@ -199,7 +200,7 @@ function createPaidWithPaymentBReq(data) { price: data.totalPaymentDue, priceCurrency: 'GBP', }, - payment: createPaymentPart(), + payment: createPaymentPart(data.paymentIdentifier), }; } @@ -221,7 +222,7 @@ function createStandardPaidBReq(data) { if (isPaymentAvailable(data)) { return { ...reqWithoutPayment, - payment: createPaymentPart(), + payment: createPaymentPart(data.paymentIdentifier), }; } return reqWithoutPayment; diff --git a/packages/openactive-integration-tests/test/templates/c1-req.js b/packages/openactive-integration-tests/test/templates/c1-req.js index e2dc3f9329..aecfa6be77 100644 --- a/packages/openactive-integration-tests/test/templates/c1-req.js +++ b/packages/openactive-integration-tests/test/templates/c1-req.js @@ -56,7 +56,7 @@ function createStandardC1Req(data) { orderItemIntakeForm: undefined, orderItemIntakeFormResponse: undefined, })), - payment: createPaymentPart(false), + payment: createPaymentPart(), }; } @@ -150,7 +150,7 @@ function createIncorrectReconciliationDetails(data) { const req = createStandardC1Req(data); // Always include payment details, regardless of if payment reconciliation // details are available in the config, as per the spec for Payment reconciliation detail validation - if (!req.payment) req.payment = createPaymentPart(false, true); + if (!req.payment) req.payment = createPaymentPart(undefined, true); if (req.payment.accountId) { req.payment.accountId = `invalid-${shortid.generate()}`; } diff --git a/packages/openactive-integration-tests/test/templates/c2-req.js b/packages/openactive-integration-tests/test/templates/c2-req.js index ffb660b26f..6aef3df814 100644 --- a/packages/openactive-integration-tests/test/templates/c2-req.js +++ b/packages/openactive-integration-tests/test/templates/c2-req.js @@ -108,7 +108,7 @@ function createStandardC2Req(data) { orderItemIntakeForm: undefined, orderItemIntakeFormResponse: undefined, })), - payment: createPaymentPart(false), + payment: createPaymentPart(), }; } @@ -264,7 +264,7 @@ function createIncorrectReconciliationDetails(data) { const req = createStandardC2Req(data); // Always include payment details, regardless of if payment reconciliation // details are available in the config, as per the spec for Payment reconciliation detail validation - if (!req.payment) req.payment = createPaymentPart(false, true); + if (!req.payment) req.payment = createPaymentPart(undefined, true); if (req.payment.accountId) { req.payment.accountId = `invalid-${shortid.generate()}`; } diff --git a/packages/openactive-integration-tests/test/templates/common.js b/packages/openactive-integration-tests/test/templates/common.js index a6598f4b5a..133a9d2034 100644 --- a/packages/openactive-integration-tests/test/templates/common.js +++ b/packages/openactive-integration-tests/test/templates/common.js @@ -1,5 +1,4 @@ const { isNil } = require('lodash'); -const shortid = require('shortid'); /** * @typedef {import('../helpers/flow-stages/fetch-opportunities').OrderItem} OrderItem @@ -18,21 +17,22 @@ const shortid = require('shortid'); */ /** - * @param {boolean} [includeIdentifier] + * @param {string | null | undefined} [identifier] * @param {boolean} [allowEmptyPaymentObject] * @returns {Payment} */ -function createPaymentPart(includeIdentifier = true, allowEmptyPaymentObject = false) { +function createPaymentPart(identifier, allowEmptyPaymentObject = false) { /** @type {Payment} */ const payment = { '@type': 'Payment' }; - if (includeIdentifier) { - payment.identifier = shortid.generate(); + const hasIdentifier = !isNil(identifier); + if (hasIdentifier) { + payment.identifier = identifier; } const { paymentReconciliationDetails } = global.SELLER_CONFIG.primary; if (!paymentReconciliationDetails) { - if (!includeIdentifier) { + if (!hasIdentifier) { if (allowEmptyPaymentObject) { return payment; }