feat: decouple cart from shared_lib, add app-owned models
Phase 1-3 of decoupling: - path_setup.py adds project root to sys.path - Cart-owned models in cart/models/ (order, page_config) - All imports updated: shared.infrastructure, shared.db, shared.browser, etc. - PageConfig uses container_type/container_id instead of post_id FK Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
0
__init__.py
Normal file
0
__init__.py
Normal file
19
app.py
19
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()
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
83
config/app-config.yaml
Normal file
83
config/app-config.yaml
Normal file
@@ -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-'
|
||||
|
||||
2
models/__init__.py
Normal file
2
models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .order import Order, OrderItem
|
||||
from .page_config import PageConfig
|
||||
119
models/order.py
Normal file
119
models/order.py
Normal file
@@ -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",
|
||||
)
|
||||
39
models/page_config.py
Normal file
39
models/page_config.py
Normal file
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user