linkbay-checkout 0.2.0
Secure multi-tenant shopping cart and checkout system for LinkBay CMS ecosystem
License: MIT
Requires Python: >=3.8
Keywords: linkbay, cms, checkout, cart, ecommerce, stripe

# LinkBay-CheckOut

**Version**: 0.2.0

Shopping cart e checkout system per e-commerce multi-tenant. Libreria leggera senza modelli database hardcoded, con sicurezza server-side e gestione inventario integrata.

## Principali capacità

- Gestione carrelli session-based con multi-tenancy
- Prezzi server-side (prevenzione manipolazione client)
- Reservazione automatica inventario (prevenzione overselling)
- Controllo concorrenza (optimistic locking)
- Integrazione Stripe per pagamenti
- Integrazione PayPal (v0.2.0)
- Cleanup automatico carrelli scaduti

## Indice

- [Installazione](#installazione)
- [Quick start](#quick-start)
- [Panoramica componenti](#panoramica-componenti)
- [Ricette d'uso](#ricette-duso)
- [Configurazione](#configurazione)
- [Troubleshooting](#troubleshooting)
- [FAQ performance e sicurezza](#faq-performance-e-sicurezza)
- [Licenza](#licenza)

## Installazione

```bash
pip install linkbay-checkout
```

| Requisito | Versione | Note |
|-----------|----------|------|
| Python | 3.8 – 3.12 | testato in CI |
| pydantic | >= 2.0.0 | per DTOs |
| stripe | >= 7.0.0 | integrazione pagamenti |
| fastapi | >= 0.104.0 (opz.) | per router |

## Quick start

```python
from linkbay_checkout import CartManager
from linkbay_checkout.security_protocols import ProductPriceProvider, InventoryService
from decimal import Decimal

# Implementa security protocols (REQUIRED)
class MyPriceProvider(ProductPriceProvider):
    def get_product_price(self, product_id, tenant_id, variant_id=None):
        # Fetch from YOUR database
        return Decimal("29.99")

class MyInventoryService(InventoryService):
    def check_availability(self, product_id, tenant_id, quantity, variant_id=None):
        # Check YOUR stock
        return True
    
    def reserve_quantity(self, product_id, tenant_id, quantity, reservation_id, ttl_minutes):
        # Reserve in YOUR system
        pass
    
    def release_reservation(self, reservation_id, tenant_id):
        # Release reservation
        pass
    
    def allocate_inventory(self, product_id, tenant_id, quantity, order_id, reservation_id=None, variant_id=None):
        # Convert reservation to hard allocation
        pass

# Inizializza CartManager
cart_manager = CartManager(
    storage=my_cart_storage,  # Your implementation
    price_provider=MyPriceProvider(),
    inventory_service=MyInventoryService()
)

# Usa
cart = cart_manager.add_item(
    session_id="sess_123",
    tenant_id="tenant_abc",  # Multi-tenancy
    product_id="prod_1",
    quantity=2
    # NO price parameter - fetched server-side
)
```

**Importante**: Questa libreria NON include modelli database. Devi implementare storage adapter e security protocols con i tuoi modelli.

## Panoramica componenti

| Componente | Ruolo | Note |
|------------|-------|------|
| CartManager | Gestione carrello (add, update, remove) | Richiede ProductPriceProvider + InventoryService |
| CheckoutManager | Flusso checkout e pagamento | Multi-provider (Stripe, PayPal) |
| StripeClient | Integrazione Stripe Checkout | Multi-tenant account support |
| PayPalClient | Integrazione PayPal | Nuovo in v0.2.0 |
| CartStorage (Protocol) | Interfaccia per persistence | User implementa con proprio DB |
| ProductPriceProvider (Protocol) | Server-side price fetching | REQUIRED per sicurezza |
| InventoryService (Protocol) | Gestione stock e reservazioni | REQUIRED per prevenire overselling |

## Ricette d'uso

### 1. Gestione carrello base

```python
# Aggiungi item
cart = cart_manager.add_item(
    session_id="sess_123",
    tenant_id="tenant_abc",
    product_id="prod_1",
    quantity=2
)

# Aggiorna quantità
cart = cart_manager.update_item_quantity(
    session_id="sess_123",
    tenant_id="tenant_abc",
    product_id="prod_1",
    quantity=5
)

# Rimuovi item
cart = cart_manager.remove_item(
    session_id="sess_123",
    tenant_id="tenant_abc",
    product_id="prod_1"
)
```

### 2. Controllo concorrenza

```python
# Fetch cart con version
cart = cart_manager.get_or_create_cart("sess_123", "tenant_abc")

# Modifica con version check
try:
    cart = cart_manager.add_item(
        session_id="sess_123",
        tenant_id="tenant_abc",
        product_id="prod_2",
        quantity=1,
        expected_version=cart.version  # Previene race conditions
    )
except ConcurrentModificationError:
    # Cart modificato da altra richiesta
    # Refresh e retry
    pass
```

### 3. Implementare storage adapter

```python
from linkbay_checkout.cart_manager import CartStorage
from linkbay_checkout.models import CartData
from typing import Optional, List, Tuple
from datetime import datetime

class SQLAlchemyCartStorage(CartStorage):
    def __init__(self, db_session):
        self.db = db_session
    
    def get_cart(self, session_id: str, tenant_id: str) -> Optional[CartData]:
        cart_row = self.db.query(Cart).filter_by(
            session_id=session_id,
            tenant_id=tenant_id
        ).first()
        
        if not cart_row:
            return None
        
        # Convert DB model → CartData DTO
        return CartData(
            session_id=cart_row.session_id,
            tenant_id=cart_row.tenant_id,
            items=[CartItem(**item) for item in cart_row.items],
            version=cart_row.version,
            expires_at=cart_row.expires_at
        )
    
    def save_cart(self, cart: CartData) -> None:
        # Convert CartData DTO → DB model and save
        cart_row = self.db.query(Cart).filter_by(
            session_id=cart.session_id,
            tenant_id=cart.tenant_id
        ).first()
        
        if cart_row:
            cart_row.update_from_dto(cart)
        else:
            cart_row = Cart.from_dto(cart)
            self.db.add(cart_row)
        
        self.db.commit()
    
    def delete_cart(self, session_id: str, tenant_id: str) -> None:
        self.db.query(Cart).filter_by(
            session_id=session_id,
            tenant_id=tenant_id
        ).delete()
        self.db.commit()
    
    def get_expired_carts(self, before: datetime) -> List[Tuple[str, str]]:
        expired = self.db.query(Cart.session_id, Cart.tenant_id).filter(
            Cart.expires_at < before
        ).all()
        return [(row.session_id, row.tenant_id) for row in expired]
```

### 4. Integrazione Stripe multi-tenant

```python
from linkbay_checkout.integrations import StripeClient
from linkbay_checkout.security_protocols import StripeAccountProvider

class MyStripeAccountProvider(StripeAccountProvider):
    def get_stripe_config(self, tenant_id: str):
        # Fetch tenant-specific Stripe credentials
        tenant = db.query(Tenant).get(tenant_id)
        return {
            "api_key": tenant.stripe_secret_key,
            "webhook_secret": tenant.stripe_webhook_secret
        }

stripe_client = StripeClient(
    account_provider=MyStripeAccountProvider()
)

# Crea Checkout Session (tenant-specific)
session = stripe_client.create_checkout_session(
    tenant_id="tenant_abc",
    amount=Decimal("99.99"),
    currency="EUR",
    success_url="https://store.com/success",
    cancel_url="https://store.com/cancel"
)
```

### 5. Cleanup carrelli scaduti

```python
# Background job (run ogni 24h)
from apscheduler.schedulers.asyncio import AsyncIOScheduler

scheduler = AsyncIOScheduler()

@scheduler.scheduled_job('cron', hour=3)  # 3 AM daily
def cleanup_job():
    count = cart_manager.cleanup_expired_carts()
    logger.info(f"Cleaned {count} expired carts")

scheduler.start()
```

## Configurazione

### CartManager

```python
CartManager(
    storage: CartStorage,              # REQUIRED
    price_provider: ProductPriceProvider,  # REQUIRED
    inventory_service: InventoryService,   # REQUIRED
    cart_expiration_days: int = 7
)
```

### CheckoutManager

```python
CheckoutManager(
    storage: CheckoutStorage,          # REQUIRED
    order_creator: OrderCreator,       # REQUIRED
    shipping_calculator: ShippingRateCalculator,  # REQUIRED
    stripe_client: Optional[StripeClient] = None,
    paypal_client: Optional[PayPalClient] = None,
    tax_calculator: Optional[TaxCalculator] = None
)
```

## Troubleshooting

| Problema | Causa | Soluzione |
|----------|-------|-----------|
| ProductNotFoundError | Prodotto non esiste o tenant_id sbagliato | Verifica product_id e tenant_id siano corretti |
| InsufficientStockError | Stock non sufficiente | Mostra messaggio user "Solo X disponibili" |
| ConcurrentModificationError | Cart modificato da altra richiesta | Refresh cart e retry operazione |
| PriceValidationError | Prezzo <= 0 dal database | Fix data: price deve essere > 0 |
| ReservationNotFoundError durante cleanup | Reservation già scaduta | Normale, ignora error (già rilasciata) |

## FAQ performance e sicurezza

**Quanto è sicuro contro manipolazione prezzi?**  
Prezzi fetched server-side via ProductPriceProvider. Client non può inviare price. 100% sicuro se provider corretto.

**Come previene overselling?**  
InventoryService riserva stock durante add_to_cart. Rilascio automatico su remove/expiration. Previene race conditions.

**Gestisce concorrenza?**  
Optimistic locking via version field. Pass expected_version per prevenire lost updates.

**Performance con molti carrelli?**  
Cleanup async via background job. Operazioni cart O(1). Scalabile a 10k+ carrelli attivi.

**Sessioni sicure?**  
Library non gestisce session ID. Usa secure cookies (httponly, samesite) lato applicazione.

## Licenza

MIT - Alessio Quagliara

## Supporto

- **Issues**: https://github.com/AlessioQuagliara/LinkBay-CheckOut/issues
- **Email**: quagliara.alessio@gmail.com

Contribuisci, apri una issue o raccontaci come stai usando la libreria 🧡