Skip to main content

Overview

The TattooSession model represents a tattoo appointment or session, linking clients and artists with scheduling information, pricing, payment tracking, and status management.

Model Definition

from sqlalchemy import String, Text, DateTime, Enum, ForeignKey, Float, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship

class TattooSession(Base):
    __tablename__ = "sessions"

Fields

Primary Key

id
int
required
Unique identifier for the session (auto-increment)

Relationships

client_id
int
required
Foreign key reference to the clientForeign Key: clients.idOn Delete: CASCADE (session deleted when client is deleted)Indexed: Yes
artist_id
int
required
Foreign key reference to the artistForeign Key: artists.idOn Delete: RESTRICT (prevents artist deletion if they have sessions)Indexed: Yes

Scheduling

start
datetime
required
Session start date and timeIndexed: Yes, for calendar queries
end
datetime
required
Session end date and timeIndexed: Yes, for calendar queries

Session Details

status
str
default:"Activa"
Current status of the sessionType: EnumValid Values:
  • “Activa” - Active/ongoing session
  • “Completada” - Completed session
  • “En espera” - Waiting/scheduled
  • “Cancelada” - Cancelled session
price
float
default:"0.0"
Total price for the session
notes
str
Additional notes about the sessionType: Text (unlimited length)Nullable: True

Commission

commission_override
float
Optional commission rate override for this specific sessionNullable: TrueUsage: If set, overrides the artist’s default rate_commissionExample: 0.55 = 55% commission for this session only

Relationship Objects

client
Client
The Client object for this sessionRelationship: Many-to-OneBack Populates: sessions on Client model
artist
Artist
The Artist object for this sessionRelationship: Many-to-OneBack Populates: sessions on Artist model
transactions
List[Transaction]
List of all payment transactions for this sessionRelationship: One-to-Many with TransactionCascade: all, delete-orphan - transactions deleted when session is deletedOrdered By: Transaction.date (chronological order)

Computed Properties

transaction

@property
def transaction(self) -> Optional[Transaction]:
    """Returns the last transaction or None (for backward compatibility)."""
transaction
Transaction | None
The most recent transaction for this sessionNote: Legacy property for backward compatibility. Use transactions list for full payment history.

total_paid

@property
def total_paid(self) -> float:
    """Sum of all non-deleted transaction amounts."""
total_paid
float
Total amount paid across all transactions (excluding deleted transactions)Calculation: Sums all transaction.amount where deleted_flag is FalseExample: If transactions are [100.0, 50.0, 25.0], returns 175.0

balance

@property
def balance(self) -> float:
    """Remaining balance (price - total_paid)."""
balance
float
Remaining balance to be paidCalculation: max(0.0, price - total_paid)Example: If price is 500.0 and total_paid is 300.0, returns 200.0Note: Never negative (minimum is 0.0)

Database Schema

CREATE TABLE sessions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    client_id INTEGER NOT NULL,
    artist_id INTEGER NOT NULL,
    start DATETIME NOT NULL,
    end DATETIME NOT NULL,
    status VARCHAR DEFAULT 'Activa',
    price FLOAT DEFAULT 0.0,
    notes TEXT,
    commission_override FLOAT,
    
    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE,
    FOREIGN KEY (artist_id) REFERENCES artists(id) ON DELETE RESTRICT
);

CREATE INDEX ix_sessions_client_id ON sessions(client_id);
CREATE INDEX ix_sessions_artist_id ON sessions(artist_id);
CREATE INDEX ix_sessions_start ON sessions(start);
CREATE INDEX ix_sessions_end ON sessions(end);
CREATE INDEX ix_sessions_artist_time ON sessions(artist_id, start, end);

Usage Examples

Create a New Session

from data.models.session_tattoo import TattooSession
from datetime import datetime, timedelta

# Create a new session
session = TattooSession(
    client_id=1,
    artist_id=2,
    start=datetime(2024, 3, 15, 10, 0),  # March 15, 2024 at 10:00 AM
    end=datetime(2024, 3, 15, 14, 0),    # March 15, 2024 at 2:00 PM
    status="En espera",
    price=500.0,
    notes="Full sleeve session - outline work"
)
db.add(session)
db.commit()

Query Sessions with Payment Info

# Get session with payment details
session = db.query(TattooSession).filter(TattooSession.id == 1).first()

print(f"Client: {session.client.name}")
print(f"Artist: {session.artist.name}")
print(f"Date: {session.start.strftime('%Y-%m-%d %H:%M')}")
print(f"Price: ${session.price:.2f}")
print(f"Total Paid: ${session.total_paid:.2f}")
print(f"Balance: ${session.balance:.2f}")
print(f"Status: {session.status}")

Get Sessions by Date Range

from datetime import datetime, timedelta

# Get all sessions for next week
today = datetime.now()
next_week = today + timedelta(days=7)

sessions = db.query(TattooSession).filter(
    TattooSession.start >= today,
    TattooSession.start < next_week,
    TattooSession.status != "Cancelada"
).order_by(TattooSession.start).all()

for session in sessions:
    print(f"{session.start} - {session.client.name} with {session.artist.name}")

Get Artist’s Sessions

# Get all active sessions for an artist
artist_sessions = db.query(TattooSession).filter(
    TattooSession.artist_id == 1,
    TattooSession.status == "Activa"
).all()

total_revenue = sum(s.price for s in artist_sessions)
total_paid = sum(s.total_paid for s in artist_sessions)

print(f"Total Sessions: {len(artist_sessions)}")
print(f"Total Revenue: ${total_revenue:.2f}")
print(f"Total Collected: ${total_paid:.2f}")

Update Session Status

# Mark session as completed
session = db.query(TattooSession).filter(TattooSession.id == 1).first()
session.status = "Completada"
db.commit()

Calculate Commission

# Get commission for a session
session = db.query(TattooSession).filter(TattooSession.id == 1).first()

# Use session-specific override or artist's default rate
commission_rate = session.commission_override or session.artist.rate_commission
artist_earnings = session.total_paid * commission_rate
studio_earnings = session.total_paid * (1 - commission_rate)

print(f"Commission Rate: {commission_rate * 100}%")
print(f"Artist Earnings: ${artist_earnings:.2f}")
print(f"Studio Earnings: ${studio_earnings:.2f}")

Status Workflow

Typical session status progression:
  1. En espera - Session is scheduled but hasn’t started
  2. Activa - Session is currently in progress
  3. Completada - Session finished successfully
  4. Cancelada - Session was cancelled

Payment Tracking

The session tracks payments through the transactions relationship:
  • Multiple payments: A session can have multiple transactions (deposits, installments)
  • total_paid: Automatically calculated from all non-deleted transactions
  • balance: Shows remaining amount (price - total_paid)
  • Full payment: When balance equals 0.0

Composite Index

The model includes a composite index for efficient artist schedule queries:
Index("ix_sessions_artist_time", 
      TattooSession.artist_id, 
      TattooSession.start, 
      TattooSession.end)
This optimizes queries like “get all sessions for artist X between dates Y and Z”.

Notes

  • Sessions cascade delete when the client is deleted (ON DELETE CASCADE)
  • Artists cannot be deleted if they have sessions (ON DELETE RESTRICT)
  • The commission_override allows per-session commission rates
  • Use total_paid and balance properties instead of calculating manually
  • All datetime fields should include timezone information in production
  • Location in codebase: data/models/session_tattoo.py:14

Build docs developers (and LLMs) love