diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app.py b/app.py index f1346dc..dea4053 100644 --- a/app.py +++ b/app.py @@ -7,22 +7,22 @@ from quart import g, abort from jinja2 import FileSystemLoader, ChoiceLoader from sqlalchemy import select -from shared.factory import create_base_app +from shared.infrastructure.factory import create_base_app -from suma_browser.app.bp import ( +from bp import ( register_cart_overview, register_page_cart, register_cart_global, register_cart_api, register_orders, ) -from suma_browser.app.bp.cart.services import ( +from bp.cart.services import ( get_cart, total, get_calendar_cart_entries, calendar_total, ) -from suma_browser.app.bp.cart.services.page_cart import ( +from bp.cart.services.page_cart import ( get_cart_for_page, get_calendar_entries_for_page, ) @@ -45,8 +45,8 @@ async def cart_context() -> dict: When g.page_post exists, cart and calendar_cart_entries are page-scoped. Global cart_count / cart_total stay global for cart-mini. """ - from shared.context import base_context - from shared.internal_api import get as api_get, dictobj + from shared.infrastructure.context import base_context + from shared.infrastructure.internal_api import get as api_get, dictobj ctx = await base_context() @@ -83,7 +83,7 @@ async def cart_context() -> dict: def create_app() -> "Quart": - from models.ghost_content import Post + from blog.models.ghost_content import Post from models.page_config import PageConfig app = create_base_app( @@ -128,7 +128,10 @@ def create_app() -> "Quart": g.page_post = post g.page_config = ( await g.s.execute( - select(PageConfig).where(PageConfig.post_id == post.id) + select(PageConfig).where( + PageConfig.container_type == "page", + PageConfig.container_id == post.id, + ) ) ).scalar_one_or_none() diff --git a/bp/cart/api.py b/bp/cart/api.py index e5f98d0..6a9bd18 100644 --- a/bp/cart/api.py +++ b/bp/cart/api.py @@ -10,12 +10,12 @@ from quart import Blueprint, g, request, jsonify from sqlalchemy import select, update, func from sqlalchemy.orm import selectinload -from models.market import CartItem -from models.market_place import MarketPlace -from models.calendars import CalendarEntry, Calendar -from models.ghost_content import Post -from suma_browser.app.csrf import csrf_exempt -from shared.cart_identity import current_cart_identity +from market.models.market import CartItem +from market.models.market_place import MarketPlace +from events.models.calendars import CalendarEntry, Calendar +from blog.models.ghost_content import Post +from shared.browser.app.csrf import csrf_exempt +from shared.infrastructure.cart_identity import current_cart_identity def register() -> Blueprint: @@ -55,7 +55,8 @@ def register() -> Blueprint: if page_post_id is not None: mp_ids = select(MarketPlace.id).where( - MarketPlace.post_id == page_post_id, + MarketPlace.container_type == "page", + MarketPlace.container_id == page_post_id, MarketPlace.deleted_at.is_(None), ).scalar_subquery() cart_q = cart_q.where(CartItem.market_place_id.in_(mp_ids)) @@ -84,7 +85,8 @@ def register() -> Blueprint: if page_post_id is not None: cal_ids = select(Calendar.id).where( - Calendar.post_id == page_post_id, + Calendar.container_type == "page", + Calendar.container_id == page_post_id, Calendar.deleted_at.is_(None), ).scalar_subquery() cal_q = cal_q.where(CalendarEntry.calendar_id.in_(cal_ids)) diff --git a/bp/cart/global_routes.py b/bp/cart/global_routes.py index 3490171..2baed80 100644 --- a/bp/cart/global_routes.py +++ b/bp/cart/global_routes.py @@ -6,7 +6,7 @@ from quart import Blueprint, g, request, render_template, redirect, url_for, mak from sqlalchemy import select from models.order import Order -from suma_browser.app.utils.htmx import is_htmx_request +from shared.browser.app.utils.htmx import is_htmx_request from .services import ( current_cart_identity, get_cart, @@ -26,8 +26,8 @@ from .services.checkout import ( validate_webhook_secret, get_order_with_details, ) -from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout -from config import config +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.config import config def register(url_prefix: str) -> Blueprint: diff --git a/bp/cart/login_helper.py b/bp/cart/login_helper.py index c5576ca..8d2134c 100644 --- a/bp/cart/login_helper.py +++ b/bp/cart/login_helper.py @@ -6,7 +6,7 @@ from quart import g, session as qsession from sqlalchemy import select from typing import Optional -from models.market import CartItem +from market.models.market import CartItem async def merge_anonymous_cart_into_user(user_id: int) -> None: diff --git a/bp/cart/overview_routes.py b/bp/cart/overview_routes.py index 937908a..15f9eb6 100644 --- a/bp/cart/overview_routes.py +++ b/bp/cart/overview_routes.py @@ -4,7 +4,7 @@ from __future__ import annotations from quart import Blueprint, render_template, make_response -from suma_browser.app.utils.htmx import is_htmx_request +from shared.browser.app.utils.htmx import is_htmx_request from .services import get_cart_grouped_by_page diff --git a/bp/cart/page_routes.py b/bp/cart/page_routes.py index f3048c2..2c8a576 100644 --- a/bp/cart/page_routes.py +++ b/bp/cart/page_routes.py @@ -4,9 +4,9 @@ from __future__ import annotations from quart import Blueprint, g, render_template, redirect, make_response, url_for -from suma_browser.app.utils.htmx import is_htmx_request -from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout -from config import config +from shared.browser.app.utils.htmx import is_htmx_request +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.config import config from .services import ( total, clear_cart_for_order, diff --git a/bp/cart/routes_old.py b/bp/cart/routes_old.py index 902cba5..6c77d74 100644 --- a/bp/cart/routes_old.py +++ b/bp/cart/routes_old.py @@ -6,9 +6,9 @@ from quart import Blueprint, g, request, render_template, redirect, url_for, mak from sqlalchemy import select, update from sqlalchemy.orm import selectinload -from models.market import Product, CartItem +from market.models.market import Product, CartItem from models.order import Order, OrderItem -from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout from .services import ( current_cart_identity, get_cart, @@ -28,9 +28,9 @@ from .services.checkout import ( validate_webhook_secret, get_order_with_details, ) -from config import config -from models.calendars import CalendarEntry # NEW -from suma_browser.app.utils.htmx import is_htmx_request +from shared.config import config +from events.models.calendars import CalendarEntry # NEW +from shared.browser.app.utils.htmx import is_htmx_request def register(url_prefix: str) -> Blueprint: bp = Blueprint("cart", __name__, url_prefix=url_prefix) diff --git a/bp/cart/services/adopt_session_cart_for_user.py b/bp/cart/services/adopt_session_cart_for_user.py index f28c2fd..7c143a2 100644 --- a/bp/cart/services/adopt_session_cart_for_user.py +++ b/bp/cart/services/adopt_session_cart_for_user.py @@ -1,6 +1,6 @@ from sqlalchemy import select, update, func -from models.market import CartItem +from market.models.market import CartItem async def adopt_session_cart_for_user(session, user_id: int, session_id: str | None) -> None: diff --git a/bp/cart/services/calendar_cart.py b/bp/cart/services/calendar_cart.py index 1731fb4..f985ff4 100644 --- a/bp/cart/services/calendar_cart.py +++ b/bp/cart/services/calendar_cart.py @@ -3,7 +3,7 @@ from __future__ import annotations from sqlalchemy import select from sqlalchemy.orm import selectinload -from models.calendars import CalendarEntry +from events.models.calendars import CalendarEntry from .identity import current_cart_identity diff --git a/bp/cart/services/check_sumup_status.py b/bp/cart/services/check_sumup_status.py index 61b9f67..a5df783 100644 --- a/bp/cart/services/check_sumup_status.py +++ b/bp/cart/services/check_sumup_status.py @@ -1,6 +1,6 @@ -from suma_browser.app.payments.sumup import get_checkout as sumup_get_checkout +from shared.browser.app.payments.sumup import get_checkout as sumup_get_checkout from sqlalchemy import update -from models.calendars import CalendarEntry +from events.models.calendars import CalendarEntry async def check_sumup_status(session, order): diff --git a/bp/cart/services/checkout.py b/bp/cart/services/checkout.py index 24fd097..d66e399 100644 --- a/bp/cart/services/checkout.py +++ b/bp/cart/services/checkout.py @@ -7,12 +7,12 @@ from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from models.market import Product, CartItem +from market.models.market import Product, CartItem from models.order import Order, OrderItem -from models.calendars import CalendarEntry, Calendar +from events.models.calendars import CalendarEntry, Calendar from models.page_config import PageConfig -from models.market_place import MarketPlace -from config import config +from market.models.market_place import MarketPlace +from shared.config import config async def find_or_create_cart_item( @@ -76,13 +76,13 @@ async def resolve_page_config( if ci.market_place_id: mp = await session.get(MarketPlace, ci.market_place_id) if mp: - post_ids.add(mp.post_id) + post_ids.add(mp.container_id) # From calendar entries via calendar for entry in calendar_entries: cal = await session.get(Calendar, entry.calendar_id) - if cal and cal.post_id: - post_ids.add(cal.post_id) + if cal and cal.container_id: + post_ids.add(cal.container_id) if len(post_ids) > 1: raise ValueError("Cannot checkout items from multiple pages") @@ -92,7 +92,10 @@ async def resolve_page_config( post_id = post_ids.pop() pc = (await session.execute( - select(PageConfig).where(PageConfig.post_id == post_id) + select(PageConfig).where( + PageConfig.container_type == "page", + PageConfig.container_id == post_id, + ) )).scalar_one_or_none() return pc @@ -158,7 +161,8 @@ async def create_order_from_cart( if page_post_id is not None: cal_ids = select(Calendar.id).where( - Calendar.post_id == page_post_id, + Calendar.container_type == "page", + Calendar.container_id == page_post_id, Calendar.deleted_at.is_(None), ).scalar_subquery() calendar_filters.append(CalendarEntry.calendar_id.in_(cal_ids)) diff --git a/bp/cart/services/clear_cart_for_order.py b/bp/cart/services/clear_cart_for_order.py index 2cba809..822d71d 100644 --- a/bp/cart/services/clear_cart_for_order.py +++ b/bp/cart/services/clear_cart_for_order.py @@ -1,7 +1,7 @@ from sqlalchemy import update, func, select -from models.market import CartItem -from models.market_place import MarketPlace +from market.models.market import CartItem +from market.models.market_place import MarketPlace from models.order import Order @@ -24,7 +24,8 @@ async def clear_cart_for_order(session, order: Order, *, page_post_id: int | Non if page_post_id is not None: mp_ids = select(MarketPlace.id).where( - MarketPlace.post_id == page_post_id, + MarketPlace.container_type == "page", + MarketPlace.container_id == page_post_id, MarketPlace.deleted_at.is_(None), ).scalar_subquery() filters.append(CartItem.market_place_id.in_(mp_ids)) diff --git a/bp/cart/services/get_cart.py b/bp/cart/services/get_cart.py index ecd16cc..1f0886b 100644 --- a/bp/cart/services/get_cart.py +++ b/bp/cart/services/get_cart.py @@ -1,7 +1,7 @@ from sqlalchemy import select from sqlalchemy.orm import selectinload -from models.market import CartItem +from market.models.market import CartItem from .identity import current_cart_identity async def get_cart(session): diff --git a/bp/cart/services/identity.py b/bp/cart/services/identity.py index a1f8594..50ecb70 100644 --- a/bp/cart/services/identity.py +++ b/bp/cart/services/identity.py @@ -1,4 +1,4 @@ # Re-export from canonical shared location -from shared.cart_identity import CartIdentity, current_cart_identity +from shared.infrastructure.cart_identity import CartIdentity, current_cart_identity __all__ = ["CartIdentity", "current_cart_identity"] diff --git a/bp/cart/services/page_cart.py b/bp/cart/services/page_cart.py index b98bb69..eb83bf6 100644 --- a/bp/cart/services/page_cart.py +++ b/bp/cart/services/page_cart.py @@ -2,7 +2,8 @@ Page-scoped cart queries. Groups cart items and calendar entries by their owning page (Post), -determined via CartItem.market_place.post_id and CalendarEntry.calendar.post_id. +determined via CartItem.market_place.container_id and CalendarEntry.calendar.container_id +(where container_type == "page"). """ from __future__ import annotations @@ -11,21 +12,22 @@ from collections import defaultdict from sqlalchemy import select from sqlalchemy.orm import selectinload -from models.market import CartItem -from models.market_place import MarketPlace -from models.calendars import CalendarEntry, Calendar -from models.ghost_content import Post +from market.models.market import CartItem +from market.models.market_place import MarketPlace +from events.models.calendars import CalendarEntry, Calendar +from blog.models.ghost_content import Post from models.page_config import PageConfig from .identity import current_cart_identity async def get_cart_for_page(session, post_id: int) -> list[CartItem]: - """Return cart items scoped to a specific page (via MarketPlace.post_id).""" + """Return cart items scoped to a specific page (via MarketPlace.container_id).""" ident = current_cart_identity() filters = [ CartItem.deleted_at.is_(None), - MarketPlace.post_id == post_id, + MarketPlace.container_type == "page", + MarketPlace.container_id == post_id, MarketPlace.deleted_at.is_(None), ] if ident["user_id"] is not None: @@ -47,13 +49,14 @@ async def get_cart_for_page(session, post_id: int) -> list[CartItem]: async def get_calendar_entries_for_page(session, post_id: int) -> list[CalendarEntry]: - """Return pending calendar entries scoped to a specific page (via Calendar.post_id).""" + """Return pending calendar entries scoped to a specific page (via Calendar.container_id).""" ident = current_cart_identity() filters = [ CalendarEntry.deleted_at.is_(None), CalendarEntry.state == "pending", - Calendar.post_id == post_id, + Calendar.container_type == "page", + Calendar.container_id == post_id, Calendar.deleted_at.is_(None), ] if ident["user_id"] is not None: @@ -99,7 +102,7 @@ async def get_cart_grouped_by_page(session) -> list[dict]: cart_items = await get_cart(session) cal_entries = await get_calendar_cart_entries(session) - # Group by post_id + # Group by container_id (all current data has container_type="page") groups: dict[int | None, dict] = defaultdict(lambda: { "post_id": None, "cart_items": [], @@ -107,16 +110,16 @@ async def get_cart_grouped_by_page(session) -> list[dict]: }) for ci in cart_items: - if ci.market_place and ci.market_place.post_id: - pid = ci.market_place.post_id + if ci.market_place and ci.market_place.container_id: + pid = ci.market_place.container_id else: pid = None groups[pid]["post_id"] = pid groups[pid]["cart_items"].append(ci) for ce in cal_entries: - if ce.calendar and ce.calendar.post_id: - pid = ce.calendar.post_id + if ce.calendar and ce.calendar.container_id: + pid = ce.calendar.container_id else: pid = None groups[pid]["post_id"] = pid @@ -135,10 +138,13 @@ async def get_cart_grouped_by_page(session) -> list[dict]: posts_by_id[p.id] = p pc_result = await session.execute( - select(PageConfig).where(PageConfig.post_id.in_(post_ids)) + select(PageConfig).where( + PageConfig.container_type == "page", + PageConfig.container_id.in_(post_ids), + ) ) for pc in pc_result.scalars().all(): - configs_by_post[pc.post_id] = pc + configs_by_post[pc.container_id] = pc # Build result list (pages first, orphan last) result = [] diff --git a/bp/order/filters/qs.py b/bp/order/filters/qs.py index 7c00336..03707e8 100644 --- a/bp/order/filters/qs.py +++ b/bp/order/filters/qs.py @@ -3,8 +3,8 @@ from quart import request from typing import Iterable, Optional, Union -from suma_browser.app.filters.qs_base import KEEP, build_qs -from suma_browser.app.filters.query_types import OrderQuery +from shared.browser.app.filters.qs_base import KEEP, build_qs +from shared.browser.app.filters.query_types import OrderQuery def decode() -> OrderQuery: diff --git a/bp/order/routes.py b/bp/order/routes.py index cd2ac69..75a35eb 100644 --- a/bp/order/routes.py +++ b/bp/order/routes.py @@ -5,14 +5,14 @@ from sqlalchemy import select, func, or_, cast, String, exists from sqlalchemy.orm import selectinload -from models.market import Product +from market.models.market import Product from models.order import Order, OrderItem -from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout -from config import config +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.config import config -from shared.http_utils import vary as _vary, current_url_without_page as _current_url_without_page -from suma_browser.app.bp.cart.services import check_sumup_status -from suma_browser.app.utils.htmx import is_htmx_request +from shared.infrastructure.http_utils import vary as _vary, current_url_without_page as _current_url_without_page +from bp.cart.services import check_sumup_status +from shared.browser.app.utils.htmx import is_htmx_request from .filters.qs import makeqs_factory, decode diff --git a/bp/orders/filters/qs.py b/bp/orders/filters/qs.py index 8f10436..984e2c3 100644 --- a/bp/orders/filters/qs.py +++ b/bp/orders/filters/qs.py @@ -3,8 +3,8 @@ from quart import request from typing import Iterable, Optional, Union -from suma_browser.app.filters.qs_base import KEEP, build_qs -from suma_browser.app.filters.query_types import OrderQuery +from shared.browser.app.filters.qs_base import KEEP, build_qs +from shared.browser.app.filters.query_types import OrderQuery def decode() -> OrderQuery: diff --git a/bp/orders/routes.py b/bp/orders/routes.py index df88eb6..fad7bcd 100644 --- a/bp/orders/routes.py +++ b/bp/orders/routes.py @@ -5,15 +5,15 @@ from sqlalchemy import select, func, or_, cast, String, exists from sqlalchemy.orm import selectinload -from models.market import Product +from market.models.market import Product from models.order import Order, OrderItem -from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout -from config import config +from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout +from shared.config import config -from shared.http_utils import vary as _vary, current_url_without_page as _current_url_without_page -from suma_browser.app.bp.cart.services import check_sumup_status -from suma_browser.app.utils.htmx import is_htmx_request -from suma_browser.app.bp import register_order +from shared.infrastructure.http_utils import vary as _vary, current_url_without_page as _current_url_without_page +from bp.cart.services import check_sumup_status +from shared.browser.app.utils.htmx import is_htmx_request +from bp import register_order from .filters.qs import makeqs_factory, decode diff --git a/config/app-config.yaml b/config/app-config.yaml new file mode 100644 index 0000000..227cc2e --- /dev/null +++ b/config/app-config.yaml @@ -0,0 +1,83 @@ +# App-wide settings +base_host: "wholesale.suma.coop" +base_login: https://wholesale.suma.coop/customer/account/login/ +base_url: https://wholesale.suma.coop/ +title: Rose Ash +coop_root: /market +coop_title: Market +blog_root: / +blog_title: all the news +cart_root: /cart +app_urls: + coop: "http://localhost:8000" + market: "http://localhost:8001" + cart: "http://localhost:8002" + events: "http://localhost:8003" +cache: + fs_root: _snapshot # <- absolute path to your snapshot dir +categories: + allow: + Basics: basics + Branded Goods: branded-goods + Chilled: chilled + Frozen: frozen + Non-foods: non-foods + Supplements: supplements + Christmas: christmas +slugs: + skip: + - "" + - customer + - account + - checkout + - wishlist + - sales + - contact + - privacy-policy + - terms-and-conditions + - delivery + - catalogsearch + - quickorder + - apply + - search + - static + - media +section-titles: + - ingredients + - allergy information + - allergens + - nutritional information + - nutrition + - storage + - directions + - preparation + - serving suggestions + - origin + - country of origin + - recycling + - general information + - additional information + - a note about prices + +blacklist: + category: + - branded-goods/alcoholic-drinks + - branded-goods/beers + - branded-goods/wines + - branded-goods/ciders + product: + - list-price-suma-current-suma-price-list-each-bk012-2-html + - ---just-lem-just-wholefoods-jelly-crystals-lemon-12-x-85g-vf067-2-html + product-details: + - General Information + - A Note About Prices + +# SumUp payment settings (fill these in for live usage) +sumup: + merchant_code: "ME4J6100" + currency: "GBP" + # Name of the environment variable that holds your SumUp API key + api_key_env: "SUMUP_API_KEY" + webhook_secret: "CHANGE_ME_TO_A_LONG_RANDOM_STRING" + checkout_reference_prefix: 'dev-' + diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..508c4b0 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from .order import Order, OrderItem +from .page_config import PageConfig diff --git a/models/order.py b/models/order.py new file mode 100644 index 0000000..a374708 --- /dev/null +++ b/models/order.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Optional, List + +from sqlalchemy import Integer, String, DateTime, ForeignKey, Numeric, func, Text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from shared.db.base import Base + + +class Order(Base): + __tablename__ = "orders" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), nullable=True) + session_id: Mapped[Optional[str]] = mapped_column(String(64), index=True, nullable=True) + + page_config_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("page_configs.id", ondelete="SET NULL"), + nullable=True, + index=True, + ) + + status: Mapped[str] = mapped_column( + String(32), + nullable=False, + default="pending", + server_default="pending", + ) + currency: Mapped[str] = mapped_column(String(16), nullable=False, default="GBP") + total_amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) + + # free-form description for the order + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, index=True) + + # SumUp reference string (what we send as checkout_reference) + sumup_reference: Mapped[Optional[str]] = mapped_column( + String(255), + nullable=True, + index=True, + ) + + # SumUp integration fields + sumup_checkout_id: Mapped[Optional[str]] = mapped_column( + String(128), + nullable=True, + index=True, + ) + sumup_status: Mapped[Optional[str]] = mapped_column(String(32), nullable=True) + sumup_hosted_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + onupdate=func.now(), + ) + + items: Mapped[List["OrderItem"]] = relationship( + "OrderItem", + back_populates="order", + cascade="all, delete-orphan", + lazy="selectin", + ) + calendar_entries: Mapped[List["CalendarEntry"]] = relationship( + "CalendarEntry", + back_populates="order", + lazy="selectin", + ) + page_config: Mapped[Optional["PageConfig"]] = relationship( + "PageConfig", + foreign_keys=[page_config_id], + lazy="selectin", + ) + + +class OrderItem(Base): + __tablename__ = "order_items" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + order_id: Mapped[int] = mapped_column( + ForeignKey("orders.id", ondelete="CASCADE"), + nullable=False, + ) + + product_id: Mapped[int] = mapped_column( + ForeignKey("products.id"), + nullable=False, + ) + product_title: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) + + quantity: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + unit_price: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) + currency: Mapped[str] = mapped_column(String(16), nullable=False, default="GBP") + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + ) + + order: Mapped["Order"] = relationship( + "Order", + back_populates="items", + ) + + # NEW: link each order item to its product + product: Mapped["Product"] = relationship( + "Product", + back_populates="order_items", + lazy="selectin", + ) diff --git a/models/page_config.py b/models/page_config.py new file mode 100644 index 0000000..adb6561 --- /dev/null +++ b/models/page_config.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Optional + +from sqlalchemy import Integer, String, Text, DateTime, func, JSON, text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from shared.db.base import Base + + +class PageConfig(Base): + __tablename__ = "page_configs" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + container_type: Mapped[str] = mapped_column( + String(32), nullable=False, server_default=text("'page'"), + ) + container_id: Mapped[int] = mapped_column(Integer, nullable=False) + + features: Mapped[dict] = mapped_column( + JSON, nullable=False, server_default="{}" + ) + + # Per-page SumUp credentials (NULL until configured) + sumup_merchant_code: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) + sumup_api_key: Mapped[Optional[str]] = mapped_column(Text(), nullable=True) + sumup_checkout_prefix: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) + + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now() + ) + deleted_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), nullable=True + ) diff --git a/path_setup.py b/path_setup.py index 1d4c9ab..c7166f7 100644 --- a/path_setup.py +++ b/path_setup.py @@ -1,7 +1,9 @@ import sys import os -# Add the shared library submodule to the Python path -_shared = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shared_lib") -if _shared not in sys.path: - sys.path.insert(0, _shared) +_app_dir = os.path.dirname(os.path.abspath(__file__)) +_project_root = os.path.dirname(_app_dir) + +for _p in (_project_root, _app_dir): + if _p not in sys.path: + sys.path.insert(0, _p)