Modules

Cart

class app.cart.CartManager[source]

Bases: object

Manage shopping carts using the Singleton design pattern.

clear_cart(cart_id: str = 'default') None[source]

Clear the contents of a cart.

Parameters:

cart_id – The identifier of the cart to clear. Default value to “default”.

get_cart(cart_id: str = 'default') Cart[source]

Return an existing cart or create a new one.

Parameters:

cart_id – The unique cart identifier. Default value to “default”.

Returns:

The cart matching the given identifier.

Return type:

Cart

static get_instance()[source]

Return the unique CartManager instance, creating it if needed.

Returns:

The singleton instance.

Return type:

CartManager

Config

Configuration module for discount and shipping strategies.

This module allows to configure which discount and shipping strategies to use for order calculations. To change the behavior, edit the file config.env

The application must be restarted for changes to take effect.

Discount stategy

class app.discount_strategy.DiscountStrategy[source]

Bases: ABC

Abstract base class for discount calculation strategies.

abstractmethod calculate_discount(subtotal: float) float[source]

Calculate the discount amount for a given subtotal.

Parameters:

subtotal – The subtotal before discount

Returns:

The discount amount (positive value)

Return type:

float

class app.discount_strategy.NoDiscount[source]

Bases: DiscountStrategy

No discount applied.

calculate_discount(subtotal: float) float[source]

Calculate the discount amount for a given subtotal.

Parameters:

subtotal – The subtotal before discount

Returns:

The discount amount (positive value)

Return type:

float

class app.discount_strategy.PercentageDiscount(percentage: float)[source]

Bases: DiscountStrategy

Apply a percentage discount.

calculate_discount(subtotal: float) float[source]

Calculate the discount amount for a given subtotal.

Parameters:

subtotal – The subtotal before discount

Returns:

The discount amount (positive value)

Return type:

float

class app.discount_strategy.ThresholdFixedDiscount(threshold: float, amount: float)[source]

Bases: DiscountStrategy

Apply a fixed discount if a threshold is reached.

calculate_discount(subtotal: float) float[source]

Calculate the discount amount for a given subtotal.

Parameters:

subtotal – The subtotal before discount

Returns:

The discount amount (positive value)

Return type:

float

Inventory

class app.inventory.InventoryManager[source]

Bases: object

Manage the product inventory using the Singleton design pattern.

decrement_stock(pid: int, qty: int) None[source]

Decrease the stock of a product.

Parameters:
  • pid – The unique product identifier.

  • qty – The quantity to decrement.

Raises:
  • ValueError – If the product is not found.

  • ValueError – If the stock is insufficient.

static get_instance()[source]

Return the unique InventoryManager instance, creating it if needed.

Returns:

The singleton instance.

Return type:

InventoryManager

get_product(pid: int) Product | None[source]

Returns a product by its identifier.

Parameters:

pid – The unique product identifier.

Returns:

The matching product, or None if not found.

Return type:

Product | None

get_reorder_threshold(pid: int) int | None[source]

Returns the reorder threshold of the product whose id is pid.

Parameters:

pid – the unique product identifier

Returns:

the matching reorder threshold, or None if not found

Return type:

int | None

has_stock(pid: int, qty: int) bool[source]

Check whether a product has enough stock.

Parameters:
  • pid – The unique product identifier.

  • qty – The required quantity.

Returns:

True if the product exists and has sufficient stock, False otherwise.

Return type:

bool

Raises:

ValueError – If qty is less than 1.

list_products() list[Product][source]

Return all products in the inventory.

Returns:

The list of all products.

Return type:

list[Product]

register(event: str, observer: object, method: Callable[[Dict[str, int]], None])[source]

Registers the observer observer for the event event.

Events description:
  • decrement: triggered when a product’s stock is decreased. Message: {“pid”: int, “amount”: int (>0)}

  • threshold: triggered when a product’s stock pass under its reorder threshold. Message: {“pid”: int}

Parameters:
  • event – the concerned event

  • observer – the observer

  • method – the method to call when event happens. It should take one positional argument (message)

Raises:

ValueError – if event is not in InventoryManager._events.

unregister(event: str, observer: object)[source]

Unregisters observer from the event event.

Parameters:
  • event – the concerned event

  • observer – the observer

class app.inventory.InventoryObserver[source]

Bases: object

Observes and log the Inventory updates. Implemented as a Singleton.

decrement(message: Dict[str, int])[source]

Handles notification from event decrement

static get_instance() InventoryObserver[source]

Return the unique InventoryObserver instance, creating it if needed.

Returns:

The singleton instance.

Return type:

InventoryObserver

get_logs() list[str][source]

Returns the list of observed actions for the event decrement

get_reorders() list[str][source]

Returns the list of observed actions for the event threshold

subscribe()[source]

Subscribes to the events of the InventoryManager

threshold(message: Dict[str, int])[source]

Handles notification from event threshold

Orders

app.orders.create_order(cart_id: str = 'default') Order[source]

Create an order from a shopping cart.

Validates stock availability for every item in the cart, decrements the inventory, computes the total price, and clears the cart.

Parameters:

cart_id – The identifier of the cart to order from. Defaults to “default”.

Returns:

The newly created order.

Return type:

Order

Raises:

ValueError – If any product in the cart has insufficient stock.

app.orders.get_machine(order_id: int) StateMachine[source]

Retrieves the state machine for an order by its ID. Raises ValueError if the order is not found.

Parameters:

order_id – The unique identifier of the state machine to retrieve.

Returns:

The StateMachine object corresponding to the given order_id.

Raises:

ValueError – If the state machine for the specified order ID is not found in the system.

app.orders.get_order(order_id: int) Order[source]

Retrieves the order by its ID. Raises ValueError if the order is not found.

Parameters:

order_id – The unique identifier of the order to retrieve.

Returns:

The Order object corresponding to the given order_id.

Raises:

ValueError – If the order with the specified ID is not found in the system.

app.orders.get_order_breakdown(order_id: int) dict[str, float][source]

Calculate the pricing breakdown for an order.

Parameters:

order_id – The ID of the order

Returns:

Breakdown with subtotal, discount, shipping, and total

Return type:

dict

app.orders.set_status(order_id: int, new_status: str) Order[source]

Updates the status of an order and triggers the state machine transition. Raises ValueError if the order is not found or if the status transition is invalid.

Parameters:
  • order_id – The unique identifier of the order to update.

  • new_status – The new status to set for the order.

Returns:

The updated Order object with the new status.

Raises:
  • ValueError – If the order with the specified ID is not found or if the status

  • transition is invalid according to the state machine rules.

Pydantic models

class app.pydantic_models.Cart(*, id: str, items: dict[int, int]=<factory>)[source]

Bases: BaseModel

add(product_id: int, quantity: int = 1) None[source]
clear() None[source]
id: str
items: dict[int, int]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

remove(product_id: int) None[source]
class app.pydantic_models.CartItem(*, product_id: int, quantity: Annotated[int, Ge(ge=1)])[source]

Bases: BaseModel

model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

product_id: int
quantity: int
class app.pydantic_models.Order(*, id: int, cart_id: str, items: list[CartItem], total: float = 0.0, status: str = 'created')[source]

Bases: BaseModel

cart_id: str
id: int
items: list[CartItem]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

status: str
total: float
classmethod valid_status(v: str) str[source]
class app.pydantic_models.Product(*, id: int, name: str, price: Annotated[float, Ge(ge=0)], stock: Annotated[int, Ge(ge=0)], reorder_threshold: Annotated[int, Ge(ge=0)] = 5, weight: Annotated[float, Ge(ge=0)] = 1.0)[source]

Bases: BaseModel

id: int
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

name: str
price: float
reorder_threshold: int
stock: int
weight: float

Shipping strategy

class app.shipping_strategy.FlatRate(amount: float)[source]

Bases: ShippingStrategy

Apply a flat rate shipping cost.

calculate_shipping(subtotal: float, items: list[tuple[int, int]]) float[source]

Calculate the shipping cost.

Parameters:
  • subtotal – The subtotal before shipping

  • items – List of (product_id, quantity) tuples

Returns:

The shipping cost

Return type:

float

class app.shipping_strategy.FreeOver(threshold: float, flat_rate: float)[source]

Bases: ShippingStrategy

Free shipping if subtotal exceeds threshold, otherwise flat rate.

calculate_shipping(subtotal: float, items: list[tuple[int, int]]) float[source]

Calculate the shipping cost.

Parameters:
  • subtotal – The subtotal before shipping

  • items – List of (product_id, quantity) tuples

Returns:

The shipping cost

Return type:

float

class app.shipping_strategy.ShippingStrategy[source]

Bases: ABC

Abstract base class for shipping cost calculation strategies.

abstractmethod calculate_shipping(subtotal: float, items: list[tuple[int, int]]) float[source]

Calculate the shipping cost.

Parameters:
  • subtotal – The subtotal before shipping

  • items – List of (product_id, quantity) tuples

Returns:

The shipping cost

Return type:

float

class app.shipping_strategy.WeightBased(rate_per_kg: float)[source]

Bases: ShippingStrategy

Calculate shipping based on total weight from product data.

calculate_shipping(subtotal: float, items: list[tuple[int, int]]) float[source]

Calculate shipping cost based on total weight of items.

Parameters:
  • subtotal – The subtotal before shipping (not used in weight-based calc)

  • items – List of (product_id, quantity) tuples

Returns:

The shipping cost

Return type:

float

Raises:

ValueError – If any product_id is not found

State machine

class app.state_machine.State[source]

Bases: ABC

Abstract base class for all states in the state machine. Each state must implement the update method to handle transitions to other states.

abstractmethod update(machine: StateMachine, status: str)[source]

Handles the transition to the next state based on the new status. :param machine: The state machine instance to update. :param status: The new status that triggers the state transition.

class app.state_machine.StateCancelled[source]

Bases: State

Represents the ‘cancelled’ state of an order. In this state, the order has been cancelled and cannot be processed further. No further transitions are allowed from this state.

update(machine: StateMachine, status: str)[source]

Handles the transition to the next state based on the new status. :param machine: The state machine instance to update. :param status: The new status that triggers the state transition.

class app.state_machine.StateCreated[source]

Bases: State

Represents the ‘created’ state of an order. In this state, the order has been created but not yet paid for. The valid transition from this state is to ‘paid’.

update(machine: StateMachine, status: str)[source]

Handles the transition to the next state based on the new status. :param machine: The state machine instance to update. :param status: The new status that triggers the state transition.

class app.state_machine.StateDelivered[source]

Bases: State

Represents the ‘delivered’ state of an order. In this state, the order has been delivered to the customer. No further transitions are allowed from this state.

update(machine: StateMachine, status: str)[source]

Handles the transition to the next state based on the new status. :param machine: The state machine instance to update. :param status: The new status that triggers the state transition.

class app.state_machine.StateMachine[source]

Bases: object

Implements a simple state machine for order processing. The states are: - created - paid - packed - shipped - delivered - cancelled

The valid transitions are: - created -> paid - paid -> packed - packed -> shipped - shipped -> delivered - paid -> cancelled - packed -> cancelled

Once an order is delivered or cancelled, no further transitions are allowed.

update(new_status: str)[source]

Updates the state machine based on the new status. This method should be called whenever the order status changes.

Parameters:

new_status – The new status that triggers the state transition.

class app.state_machine.StatePacked[source]

Bases: State

Represents the ‘packed’ state of an order. In this state, the order has been packed but not yet shipped. The valid transitions from this state are to ‘shipped’ or ‘cancelled’.

update(machine: StateMachine, status: str)[source]

Handles the transition to the next state based on the new status. :param machine: The state machine instance to update. :param status: The new status that triggers the state transition.

class app.state_machine.StatePaid[source]

Bases: State

Represents the ‘paid’ state of an order. In this state, the order has been paid for but not yet packed. The valid transitions from this state are to ‘packed’ or ‘cancelled’.

update(machine: StateMachine, status: str)[source]

Handles the transition to the next state based on the new status. :param machine: The state machine instance to update. :param status: The new status that triggers the state transition.

class app.state_machine.StateShipped[source]

Bases: State

Represents the ‘shipped’ state of an order. In this state, the order has been shipped but not yet delivered. The valid transition from this state is to ‘delivered’.

update(machine: StateMachine, status: str)[source]

Handles the transition to the next state based on the new status. :param machine: The state machine instance to update. :param status: The new status that triggers the state transition.