Skip to content

Add manual order completion; refactor Stripe event handling - #3855

Open
jkachel wants to merge 10 commits into
mainfrom
jkachel/12694-manual-order-completion
Open

Add manual order completion; refactor Stripe event handling#3855
jkachel wants to merge 10 commits into
mainfrom
jkachel/12694-manual-order-completion

Conversation

@jkachel

@jkachel jkachel commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

What are the relevant tickets?

Closes mitodl/hq#12694

Description (What does it do?)

Adds the ability to administratively fulfill an order using the existing management command. This also fixes some issues with the command itself, including adding support for Stripe transactions (which isn't something it supported).

In addition, this refactors some of the Stripe implementation - there were some things that were problematic about the existing implementation:

  • It should now store the initial CheckoutSession before delivering back the checkout payload. If we don't have the CheckoutSession ID, the app might end up stuck later if we need to refund the transaction or something.
  • Updated the event handling logic to be more robust.
  • Added some additional processing helper functions to better determine the status of the CheckoutSession. Before, it was just considering the status fields in the CheckoutSession itself; to get the whole story, it should also be looking at the PaymentIntent to see what its status is too, because that's where the detail of the payment itself is stored.

How can this be tested?

Payment collected, but order status not updated:

Create an order, and then complete the payment process but do not allow the app to be aware of that. The easiest way to do this is:

  • For CyberSource, stop your MITx Online instance once you've been sent to CyberSource. The POST to take you back to the app will now fail. If you've configured your instance to get webhooks from CyberSource, disable that functionality as well.
  • For Stripe, ensure your local instance won't receive webhook events. You can either turn them off in the Stripe dashboard, turn off whatever publicly-available routing you have for that, or you can just not use the Stripe CLI command to redirect those to your local machine.

Then, run resolve_pending_order for the order in question. (For Stripe, do not re-enable your webhooks - you don't want it to retry sending events.) The app should reach out to the payment processor, determine that the order is complete, and then fulfill the order (or cancel it, depending on what you did).

Forcing fulfillment of a PendingOrder, regardless of payment state:

Create an order, and then return to the app once you get to the payment processor. For ease of testing, manually type your local instance's URL into the address bar. This should leave the order in the Pending state.

Then, run resolve_pending_order --no-check against the order in question. It should be set to Fulfilled and the order should be fulfilled.

Disabling fulfillment:

For both of these scenarios, test the skip fulfillment option by specifying --no-fulfillment when running the management command. This should still mark the order as Fulfilled but it should not make any enrollment changes.

Additional Context

A lot of the Stripe refactoring ended up in a bunch of constants and some helper functions that don't really belong in the app. Once these are shaken out a bit, they should move into the ol_django PaymentGateway app.

…ight; finish out the new stripe event/checkout session processing stuff; update its tests too

There were a couple of places where the default gateway setting was coming out of main.settings, which means the mocked one doesn't work.. and a few places where it would have been bette
r to specify the payment gateway explicitly, so did all that. Still have some more Stripe mock object testing stuff to finish out.
@gitguardian

gitguardian Bot commented Aug 14, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
36131093 Triggered Generic High Entropy Secret 5405629 ecommerce/fixtures.py View secret
36131093 Triggered Generic High Entropy Secret a85cfc1 ecommerce/fixtures.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@github-actions

Copy link
Copy Markdown

OpenAPI Changes

Show/hide changes
## Changes for v0.yaml:
12 changes: 0 error, 6 warning, 6 info
warning	[response-property-all-of-removed] at head/openapi/specs/v0.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		removed `#/components/schemas/ExtendedLegalAddress` from the `results/items/redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v0.yaml
	in API POST /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `201`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v0.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v0.yaml
	in API PATCH /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v0.yaml
	in API PUT /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v0.yaml
	in API GET /api/v0/orders/receipt/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v0.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		the `results/items/redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v0.yaml
	in API POST /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `201`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v0.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v0.yaml
	in API PATCH /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v0.yaml
	in API PUT /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v0.yaml
	in API GET /api/v0/orders/receipt/{id}/
		the `purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.



## Changes for v1.yaml:
12 changes: 0 error, 6 warning, 6 info
warning	[response-property-all-of-removed] at head/openapi/specs/v1.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		removed `#/components/schemas/ExtendedLegalAddress` from the `results/items/redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v1.yaml
	in API POST /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `201`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v1.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v1.yaml
	in API PATCH /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v1.yaml
	in API PUT /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v1.yaml
	in API GET /api/v0/orders/receipt/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v1.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		the `results/items/redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v1.yaml
	in API POST /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `201`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v1.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v1.yaml
	in API PATCH /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v1.yaml
	in API PUT /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v1.yaml
	in API GET /api/v0/orders/receipt/{id}/
		the `purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.



## Changes for v2.yaml:
12 changes: 0 error, 6 warning, 6 info
warning	[response-property-all-of-removed] at head/openapi/specs/v2.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		removed `#/components/schemas/ExtendedLegalAddress` from the `results/items/redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v2.yaml
	in API POST /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `201`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v2.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v2.yaml
	in API PATCH /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v2.yaml
	in API PUT /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `redeemed_order/purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

warning	[response-property-all-of-removed] at head/openapi/specs/v2.yaml
	in API GET /api/v0/orders/receipt/{id}/
		removed `#/components/schemas/ExtendedLegalAddress` from the `purchaser` response property `allOf` list for the response status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v2.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		the `results/items/redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v2.yaml
	in API POST /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `201`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v2.yaml
	in API GET /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v2.yaml
	in API PATCH /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v2.yaml
	in API PUT /api/v0/discounts/{parent_lookup_redeemed_discount}/redemptions/{id}/
		the `redeemed_order/purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.

info	[response-property-type-specialized] at head/openapi/specs/v2.yaml
	in API GET /api/v0/orders/receipt/{id}/
		the `purchaser` response's property `type` was narrowed from `any` to `array<object>` for status `200`
		This compared the `allOf` branches one at a time, so another branch may still guarantee what this one dropped. Run with --flatten-allof to merge the branches and compare what they describe together.



Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

@jkachel
jkachel marked this pull request as ready for review August 17, 2026 17:29
@dsubak
dsubak self-requested a review August 18, 2026 13:25
Comment thread ecommerce/api.py Outdated
Comment thread ecommerce/api.py Outdated
Comment thread ecommerce/api.py Outdated
checkout_session_id = session_transaction.data.get("id")

if (
session_transaction.data.get("object") != "checkout.session"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should checkout.session be a constant?

Comment thread ecommerce/api.py Outdated
Comment on lines +1404 to +1407
"checkout.session.completed",
"checkout.session.expired",
"checkout.session.async_payment_failed",
"checkout.session.async_payment_succeeded",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to pull this into the constants file too

Comment thread ecommerce/models.py
}

if self.order.total_price_paid == 0:
if self.order.total_price_paid == 0 or payment_data.get("is_administrative"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be an issue on my end so my apologies if that is the case, but when trying to go through a normal checkout with stripe as my payment gateway this line explodes because payment_data is a CheckoutSession StripeObject and is_administrative isn't in that data (and I guess .get throws an error since they stopped inheriting from dict quite recently)

Does this seem like a real issue or is everything working as expected on your setup?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem was that generate_checkout_payload saves the checkout session now (so we can maybe look it up later if we need, for out-of-band fulfillment) - it needed to be converted to dict first though and it didn't do that. So, real issue that is now fixed. The other places where the session data gets store were doing this properly so it was just the new one that was broken.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome; that's mostly in line with what I pieced together and makes total sense. I'll put it back through it's paces first thing tomorrow and as long as everything looks good, I think this'll be good to go!

jkachel and others added 3 commits August 18, 2026 15:03
Co-authored-by: Dan Subak <dsubak@users.noreply.github.com>
Co-authored-by: Dan Subak <dsubak@users.noreply.github.com>
…re we save it in generate_checkout_payload; add more constants
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants