Charge Payment and Roll Back on Failure

Step 2: Charge Payment and Roll Back on Failure

A facade does not just forward calls. It owns the ordering and the cleanup between subsystems. Once inventory is reserved, the money has to move, and if the charge fails the reserved stock must be handed back. Otherwise a declined card would silently eat inventory.

Extend place_order so that after a successful reservation it charges the customer:

  • Call self.payment.charge(amount). It returns True and lowers the balance when there are enough funds, False otherwise.
  • If the charge fails, undo the reservation with self.inventory.release(item, qty) and return the dict with ok set to False and reason set to payment_declined.
  • If the charge succeeds, keep returning the dict with ok set to True for now.

This rollback is the heart of the pattern: callers get all-or-nothing behavior without ever seeing the two subsystems.

Hints
Starter code
class Inventory:
    def __init__(self, stock):
        self.stock = dict(stock)

    def reserve(self, item, qty):
        if self.stock.get(item, 0) >= qty:
            self.stock[item] -= qty
            return True
        return False

    def release(self, item, qty):
        self.stock[item] = self.stock.get(item, 0) + qty


class Payment:
    def __init__(self, balance):
        self.balance = balance

    def charge(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            return True
        return False


class Shipping:
    def schedule(self, item):
        return "trk-" + item


class CheckoutFacade:
    def __init__(self, inventory, payment, shipping):
        self.inventory = inventory
        self.payment = payment
        self.shipping = shipping
        self.orders = []

    def place_order(self, item, qty, amount):
        if not self.inventory.reserve(item, qty):
            return {"ok": False, "reason": "out_of_stock"}
        # TODO: charge the customer for amount using self.payment.charge(amount).
        # If the charge fails, release the reserved inventory (rollback) with
        # self.inventory.release(item, qty) and return
        # {"ok": False, "reason": "payment_declined"}.
        return {"ok": True}
Write it on a laptop

The editor and the test runner need a wide screen. Open this page on a desktop browser and your progress will be waiting.