diff --git a/boxoffice/models/category.py b/boxoffice/models/category.py index 51e8330e..cb0c8c57 100644 --- a/boxoffice/models/category.py +++ b/boxoffice/models/category.py @@ -6,11 +6,12 @@ from uuid import UUID from . import BaseScopedNameMixin, Mapped, Model, relationship, sa +from .user import User __all__ = ['Category'] -class Category(BaseScopedNameMixin, Model): +class Category(BaseScopedNameMixin[int, User], Model): __tablename__ = 'category' __table_args__ = ( sa.UniqueConstraint('item_collection_id', 'name'), diff --git a/boxoffice/models/discount_policy.py b/boxoffice/models/discount_policy.py index 886b5002..30f21cee 100644 --- a/boxoffice/models/discount_policy.py +++ b/boxoffice/models/discount_policy.py @@ -27,6 +27,7 @@ sa, ) from .enums import DiscountTypeEnum, LineItemStatus +from .user import Organization, User __all__ = ['DiscountPolicy', 'DiscountCoupon', 'item_discount_policy'] @@ -49,7 +50,7 @@ ) -class DiscountPolicy(BaseScopedNameMixin, Model): +class DiscountPolicy(BaseScopedNameMixin[UUID, User], Model): """ Consists of the discount rules applicable on tickets. @@ -57,7 +58,6 @@ class DiscountPolicy(BaseScopedNameMixin, Model): """ __tablename__ = 'discount_policy' - __uuid_primary_key__ = True __table_args__ = ( sa.UniqueConstraint('organization_id', 'name'), sa.UniqueConstraint('organization_id', 'discount_code_base'), @@ -305,9 +305,8 @@ def generate_coupon_code(size=6, chars=string.ascii_uppercase + string.digits): return ''.join(secrets.choice(chars) for _ in range(size)) -class DiscountCoupon(IdMixin, Model): +class DiscountCoupon(IdMixin[UUID], Model): __tablename__ = 'discount_coupon' - __uuid_primary_key__ = True __table_args__ = (sa.UniqueConstraint('discount_policy_id', 'code'),) def __init__(self, *args, **kwargs): @@ -370,4 +369,3 @@ class PolicyCoupon: if TYPE_CHECKING: from .item import Item, Price - from .user import Organization, User diff --git a/boxoffice/models/invoice.py b/boxoffice/models/invoice.py index 6db792f3..ae16a8ba 100644 --- a/boxoffice/models/invoice.py +++ b/boxoffice/models/invoice.py @@ -8,6 +8,7 @@ from . import BaseMixin, Mapped, Model, UuidMixin, relationship, sa, timestamptz from .enums import InvoiceStatus +from .user import Organization, User from .utils import get_fiscal_year __all__ = ['Invoice'] @@ -25,9 +26,8 @@ def gen_invoice_no(organization, jurisdiction, invoice_dt): ) -class Invoice(UuidMixin, BaseMixin, Model): +class Invoice(UuidMixin, BaseMixin[UUID, User], Model): __tablename__ = 'invoice' - __uuid_primary_key__ = True __table_args__ = ( sa.UniqueConstraint( 'organization_id', 'fy_start_at', 'fy_end_at', 'invoice_no' @@ -143,4 +143,3 @@ def validate_immutable_final_invoice(self, key, val): if TYPE_CHECKING: from .order import Order - from .user import Organization diff --git a/boxoffice/models/item.py b/boxoffice/models/item.py index 80318e90..efa8ecb0 100644 --- a/boxoffice/models/item.py +++ b/boxoffice/models/item.py @@ -29,13 +29,13 @@ from .category import Category from .discount_policy import item_discount_policy from .enums import LineItemStatus +from .user import User __all__ = ['Item', 'Price'] -class Item(BaseScopedNameMixin, Model): +class Item(BaseScopedNameMixin[UUID, User], Model): __tablename__ = 'item' - __uuid_primary_key__ = True __table_args__ = (sa.UniqueConstraint('item_collection_id', 'name'),) description = MarkdownColumn('description', default='', nullable=False) @@ -219,9 +219,8 @@ def demand_curve(self) -> Sequence[sa.engine.Row[tuple[Decimal, int]]]: ).fetchall() -class Price(BaseScopedNameMixin, Model): +class Price(BaseScopedNameMixin[UUID, User], Model): __tablename__ = 'price' - __uuid_primary_key__ = True __table_args__ = ( sa.UniqueConstraint('item_id', 'name'), sa.CheckConstraint('start_at < end_at', 'price_start_at_lt_end_at_check'), @@ -289,4 +288,3 @@ def tense(self): if TYPE_CHECKING: from .discount_policy import DiscountPolicy from .item_collection import ItemCollection - from .user import User diff --git a/boxoffice/models/item_collection.py b/boxoffice/models/item_collection.py index 8675c36a..03af8a1c 100644 --- a/boxoffice/models/item_collection.py +++ b/boxoffice/models/item_collection.py @@ -2,6 +2,7 @@ from decimal import Decimal from typing import TYPE_CHECKING, cast +from uuid import UUID from sqlalchemy.ext.orderinglist import ordering_list @@ -17,17 +18,16 @@ ) from .enums import LineItemStatus, TransactionTypeEnum from .payment import PaymentTransaction -from .user import Organization +from .user import Organization, User from .utils import HeadersAndDataTuple __all__ = ['ItemCollection'] -class ItemCollection(BaseScopedNameMixin, Model): +class ItemCollection(BaseScopedNameMixin[UUID, User], Model): """Represent a collection of tickets.""" __tablename__ = 'item_collection' - __uuid_primary_key__ = True __table_args__ = (sa.UniqueConstraint('organization_id', 'name'),) description = MarkdownColumn('description', default='', nullable=False) diff --git a/boxoffice/models/line_item.py b/boxoffice/models/line_item.py index 1de6b8b0..5c076dd9 100644 --- a/boxoffice/models/line_item.py +++ b/boxoffice/models/line_item.py @@ -26,6 +26,7 @@ timestamptz, ) from .enums import LineItemStatus +from .user import User __all__ = ['LineItem', 'Assignee'] @@ -46,7 +47,7 @@ class LineItemDict(TypedDict): ticket_id: str -class Assignee(BaseMixin, Model): +class Assignee(BaseMixin[int, User], Model): __tablename__ = 'assignee' __table_args__ = ( sa.UniqueConstraint('line_item_id', 'current'), @@ -67,7 +68,7 @@ class Assignee(BaseMixin, Model): current: Mapped[bool | None] = sa.orm.mapped_column() -class LineItem(BaseMixin, Model): +class LineItem(BaseMixin[UUID, User], Model): """ A line item in a sale receipt. @@ -76,7 +77,6 @@ class LineItem(BaseMixin, Model): """ __tablename__ = 'line_item' - __uuid_primary_key__ = True __table_args__ = ( sa.UniqueConstraint('customer_order_id', 'line_item_seq'), sa.UniqueConstraint('previous_id'), @@ -393,4 +393,3 @@ def sales_delta(user_tz: tzinfo, ticket_ids: Sequence[str]): from .discount_policy import DiscountCoupon, DiscountPolicy from .item_collection import ItemCollection from .order import Order - from .user import User diff --git a/boxoffice/models/line_item_discounter.py b/boxoffice/models/line_item_discounter.py index 546a7e46..c9ba5510 100644 --- a/boxoffice/models/line_item_discounter.py +++ b/boxoffice/models/line_item_discounter.py @@ -6,8 +6,6 @@ from uuid import UUID import itertools -from beartype import beartype - from .discount_policy import DiscountCoupon, DiscountPolicy, PolicyCoupon from .line_item import LineItemTuple @@ -15,7 +13,6 @@ class LineItemDiscounter: - @beartype def get_discounted_line_items( self, line_items: Sequence[LineItemTuple], @@ -36,7 +33,6 @@ def get_discounted_line_items( return self.apply_discount(valid_discounts[0], line_items) return line_items - @beartype def get_valid_discounts( self, line_items: Sequence[LineItemTuple], coupons: Sequence[str] ) -> Sequence[PolicyCoupon]: @@ -51,7 +47,6 @@ def get_valid_discounts( return DiscountPolicy.get_from_ticket(ticket, len(line_items), coupons) - @beartype def calculate_discounted_amount( self, discount_policy: DiscountPolicy, line_item: LineItemTuple ) -> Decimal: @@ -73,11 +68,9 @@ def calculate_discounted_amount( return line_item.base_amount - discounted_price.amount return (discount_policy.percentage or 0) * line_item.base_amount / Decimal(100) - @beartype def is_coupon_usable(self, coupon: DiscountCoupon, applied_to_count: int) -> bool: return (coupon.usage_limit - coupon.used_count) > applied_to_count - @beartype def apply_discount( self, policy_coupon: PolicyCoupon, @@ -134,7 +127,6 @@ def apply_discount( discounted_line_items.append(line_item) return discounted_line_items - @beartype def apply_combo_discount( self, discounts: list[PolicyCoupon], @@ -149,7 +141,6 @@ def apply_combo_discount( [discounts[0]], self.apply_combo_discount(discounts[1:], line_items) ) - @beartype def apply_max_discount( self, discounts: list[PolicyCoupon | Sequence[PolicyCoupon]], @@ -184,7 +175,6 @@ def apply_max_discount( ), ) - @beartype def get_combos( self, discounts: list[PolicyCoupon], qty: int ) -> list[list[PolicyCoupon]]: diff --git a/boxoffice/models/order.py b/boxoffice/models/order.py index e6cf028a..deef53ff 100644 --- a/boxoffice/models/order.py +++ b/boxoffice/models/order.py @@ -4,7 +4,7 @@ from decimal import Decimal from functools import partial from typing import TYPE_CHECKING -from uuid import UUID, uuid4 +from uuid import UUID import secrets from sqlalchemy.ext.orderinglist import ordering_list @@ -24,6 +24,7 @@ ) from .enums import LineItemStatus, OrderStatus, TransactionTypeEnum from .line_item import LineItem +from .user import Organization, User __all__ = ['Order', 'OrderSession'] @@ -42,17 +43,13 @@ def gen_receipt_no(organization): ) -class Order(BaseMixin, Model): +class Order(BaseMixin[UUID, User], Model): __tablename__ = 'customer_order' - __uuid_primary_key__ = True __table_args__ = ( sa.UniqueConstraint('organization_id', 'invoice_no'), sa.UniqueConstraint('access_token'), ) - id: Mapped[UUID] = sa.orm.mapped_column( # type: ignore[assignment] # noqa: A003 - primary_key=True, default=uuid4 - ) user_id: Mapped[int | None] = sa.orm.mapped_column(sa.ForeignKey('user.id')) user: Mapped[User | None] = relationship(back_populates='orders') menu_id: Mapped[UUID] = sa.orm.mapped_column( @@ -99,8 +96,8 @@ class Order(BaseMixin, Model): confirmed_line_items: DynamicMapped[LineItem] = relationship( lazy='dynamic', - primaryjoin=sa.and_( - LineItem.customer_order_id == id, + primaryjoin=lambda: sa.and_( + LineItem.customer_order_id == Order.id, LineItem.status == LineItemStatus.CONFIRMED, ), viewonly=True, @@ -108,8 +105,8 @@ class Order(BaseMixin, Model): confirmed_and_cancelled_line_items: DynamicMapped[LineItem] = relationship( lazy='dynamic', - primaryjoin=sa.and_( - LineItem.customer_order_id == id, + primaryjoin=lambda: sa.and_( + LineItem.customer_order_id == Order.id, LineItem.status.in_( [LineItemStatus.CONFIRMED.value, LineItemStatus.CANCELLED.value] ), @@ -119,8 +116,8 @@ class Order(BaseMixin, Model): initial_line_items: DynamicMapped[LineItem] = relationship( lazy='dynamic', - primaryjoin=sa.and_( - LineItem.customer_order_id == id, + primaryjoin=lambda: sa.and_( + LineItem.customer_order_id == Order.id, LineItem.previous_id.is_(None), LineItem.status.in_( [ @@ -221,11 +218,10 @@ def net_amount(self) -> Decimal: return self.paid_amount - self.refunded_amount -class OrderSession(BaseMixin, Model): +class OrderSession(BaseMixin[UUID, User], Model): """Records the referrer and utm headers for an order.""" __tablename__ = 'order_session' - __uuid_primary_key__ = True customer_order_id: Mapped[UUID] = sa.orm.mapped_column( sa.ForeignKey('customer_order.id'), index=True, unique=False @@ -256,4 +252,3 @@ class OrderSession(BaseMixin, Model): from .invoice import Invoice from .item_collection import ItemCollection from .payment import OnlinePayment, PaymentTransaction - from .user import Organization, User diff --git a/boxoffice/models/payment.py b/boxoffice/models/payment.py index 9d436659..74fe12ab 100644 --- a/boxoffice/models/payment.py +++ b/boxoffice/models/payment.py @@ -25,15 +25,15 @@ TransactionMethodEnum, TransactionTypeEnum, ) +from .user import User __all__ = ['OnlinePayment', 'PaymentTransaction'] -class OnlinePayment(BaseMixin, Model): +class OnlinePayment(BaseMixin[UUID, User], Model): """Represents payments made through a payment gateway. Supports Razorpay only.""" __tablename__ = 'online_payment' - __uuid_primary_key__ = True customer_order_id: Mapped[UUID] = sa.orm.mapped_column( sa.ForeignKey('customer_order.id') ) @@ -60,7 +60,7 @@ def fail(self) -> None: self.failed_at = func.utcnow() -class PaymentTransaction(BaseMixin, Model): +class PaymentTransaction(BaseMixin[UUID, User], Model): """ Models transactions made by a customer. @@ -68,7 +68,6 @@ class PaymentTransaction(BaseMixin, Model): """ __tablename__ = 'payment_transaction' - __uuid_primary_key__ = True customer_order_id: Mapped[UUID] = sa.orm.mapped_column( sa.ForeignKey('customer_order.id') diff --git a/boxoffice/models/user.py b/boxoffice/models/user.py index 18ec218d..bab1132d 100644 --- a/boxoffice/models/user.py +++ b/boxoffice/models/user.py @@ -84,6 +84,8 @@ def permissions(self, actor, inherited=None): def fetch_invoices(self): """Return invoices for an organization as a tuple of (row_headers, rows).""" + from .order import Order # pylint: disable=import-outside-toplevel + headers = [ 'order_id', 'receipt_no', @@ -135,9 +137,10 @@ def fetch_invoices(self): # Tail imports from .invoice import Invoice # isort:skip -from .order import Order # isort:skip + if TYPE_CHECKING: from .discount_policy import DiscountPolicy from .item_collection import ItemCollection from .line_item import Assignee + from .order import Order diff --git a/requirements.txt b/requirements.txt index 9f341de5..406e889b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ git+https://github.com/hasgeek/baseframe#egg=baseframe -beartype click git+https://github.com/hasgeek/coaster#egg=coaster Flask