Skip to main content

Overview

The Transaction model represents a payment transaction in the tattoo studio, tracking payments for sessions, commission amounts, payment methods, and maintaining full audit history with soft delete capability.

Model Definition

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

class Transaction(Base):
    __tablename__ = "transactions"

Fields

Primary Key

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

Relationships

session_id
int
Foreign key reference to the tattoo sessionForeign Key: sessions.idOn Delete: SET NULL (transaction remains if session is deleted)Nullable: True (allows transactions not tied to sessions)Indexed: Yes
artist_id
int
required
Foreign key reference to the artistForeign Key: artists.idOn Delete: RESTRICT (prevents artist deletion if they have transactions)Indexed: Yes

Payment Information

amount
float
required
Total amount collected in this transactionCurrency: Assumes studio’s default currency
method
str
required
Payment method usedLength: Maximum 30 charactersCommon Values: “Efectivo” (Cash), “Tarjeta” (Card), “Transferencia” (Transfer)
concept
str
default:""
Free-form description or notes about the transactionLength: Maximum 200 characters

Timing

date
datetime
default:"func.now()"
When the payment was collectedIndexed: Yes, for reporting and groupingDefault: Current timestamp

Commission

commission_amount
float
Calculated commission amount for this transactionNullable: TrueCalculation: Typically amount * commission_rateNote: Pre-calculated and stored for historical accuracy

Soft Delete

deleted_flag
bool
default:"false"
Marks transaction as deleted without removing from databaseUsage: Allows “undoing” accidental deletions and maintains audit history

Audit Timestamps

created_at
datetime
default:"func.now()"
When the transaction record was createdAuto-generated: Yes
updated_at
datetime
default:"func.now()"
When the transaction record was last modifiedAuto-updated: Yes (on update)

Relationship Objects

session
TattooSession
The TattooSession object this transaction belongs toRelationship: Many-to-OneLazy Loading: joined (eager loading)Back Populates: transactions on TattooSession model
artist
Artist
The Artist object who performed the workRelationship: Many-to-OneLazy Loading: joined (eager loading)

Database Schema

CREATE TABLE transactions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    session_id INTEGER,
    artist_id INTEGER NOT NULL,
    amount FLOAT NOT NULL,
    method VARCHAR(30) NOT NULL,
    concept VARCHAR(200) NOT NULL DEFAULT '',
    date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    commission_amount FLOAT,
    deleted_flag BOOLEAN NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE SET NULL,
    FOREIGN KEY (artist_id) REFERENCES artists(id) ON DELETE RESTRICT
);

CREATE INDEX ix_transactions_session_id ON transactions(session_id);
CREATE INDEX ix_transactions_artist_id ON transactions(artist_id);
CREATE INDEX ix_tx_date ON transactions(date);
CREATE INDEX ix_tx_artist_date ON transactions(artist_id, date);

Usage Examples

Create a New Transaction

from data.models.transaction import Transaction
from datetime import datetime

# Create transaction for a session
transaction = Transaction(
    session_id=1,
    artist_id=2,
    amount=250.0,
    method="Efectivo",
    concept="Deposit for sleeve session",
    commission_amount=125.0  # 50% commission
)
db.add(transaction)
db.commit()

Calculate and Store Commission

# Get session and calculate commission
session = db.query(TattooSession).filter(TattooSession.id == 1).first()
commission_rate = session.commission_override or session.artist.rate_commission

# Create transaction with calculated commission
payment_amount = 500.0
transaction = Transaction(
    session_id=session.id,
    artist_id=session.artist_id,
    amount=payment_amount,
    method="Tarjeta",
    concept="Full payment",
    commission_amount=payment_amount * commission_rate
)
db.add(transaction)
db.commit()

Query Transactions for Reporting

from datetime import datetime, timedelta

# Get all transactions for current month (excluding deleted)
start_of_month = datetime.now().replace(day=1, hour=0, minute=0, second=0)

transactions = db.query(Transaction).filter(
    Transaction.date >= start_of_month,
    Transaction.deleted_flag == False
).order_by(Transaction.date).all()

total_revenue = sum(t.amount for t in transactions)
total_commissions = sum(t.commission_amount or 0 for t in transactions)

print(f"Monthly Revenue: ${total_revenue:.2f}")
print(f"Total Commissions: ${total_commissions:.2f}")
print(f"Studio Net: ${total_revenue - total_commissions:.2f}")

Get Artist’s Earnings

# Calculate artist earnings for a period
from sqlalchemy import func

artist_id = 1
start_date = datetime(2024, 3, 1)
end_date = datetime(2024, 3, 31)

earnings = db.query(
    func.sum(Transaction.commission_amount)
).filter(
    Transaction.artist_id == artist_id,
    Transaction.date >= start_date,
    Transaction.date < end_date,
    Transaction.deleted_flag == False
).scalar() or 0.0

print(f"Artist earnings for March: ${earnings:.2f}")

Soft Delete a Transaction

# Mark transaction as deleted
transaction = db.query(Transaction).filter(Transaction.id == 1).first()
transaction.deleted_flag = True
db.commit()

# Note: updated_at will automatically be updated

Restore a Deleted Transaction

# Undelete a transaction
transaction = db.query(Transaction).filter(Transaction.id == 1).first()
transaction.deleted_flag = False
db.commit()

Get Session Payment History

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

print(f"Payment history for {session.client.name}:")
for txn in session.transactions:
    if not txn.deleted_flag:
        print(f"  {txn.date.strftime('%Y-%m-%d')}: ${txn.amount:.2f} via {txn.method}")
        if txn.concept:
            print(f"    Note: {txn.concept}")

print(f"\nTotal Paid: ${session.total_paid:.2f}")
print(f"Balance: ${session.balance:.2f}")

Payment Methods Report

from sqlalchemy import func

# Group transactions by payment method
method_totals = db.query(
    Transaction.method,
    func.sum(Transaction.amount).label('total'),
    func.count(Transaction.id).label('count')
).filter(
    Transaction.deleted_flag == False
).group_by(Transaction.method).all()

print("Payment Methods Summary:")
for method, total, count in method_totals:
    print(f"{method}: {count} transactions, ${total:.2f}")

Database Indexes

The model includes several indexes for performance:
  1. ix_transactions_session_id - Fast lookups by session
  2. ix_transactions_artist_id - Fast lookups by artist
  3. ix_tx_date - Date-based reporting queries
  4. ix_tx_artist_date - Composite index for artist earnings reports

Soft Delete Pattern

The deleted_flag field implements soft delete: Benefits:
  • Maintains complete audit history
  • Allows “undo” functionality
  • Preserves financial records for accounting
  • No data loss from accidental deletion
Usage:
  • Always filter by deleted_flag == False in queries
  • Set deleted_flag = True instead of deleting records
  • Include deleted records only in admin/audit views

Commission Calculation

The commission_amount field stores the calculated commission at transaction time:
# Commission is stored, not recalculated
commission_rate = session.commission_override or session.artist.rate_commission
commission_amount = payment_amount * commission_rate
Why store instead of calculate?
  • Commission rates may change over time
  • Provides accurate historical data
  • Faster reporting queries (no joins required)
  • Maintains contractual accuracy

Audit Trail

The model includes full audit capabilities:
  • created_at - When transaction was recorded
  • updated_at - When transaction was last modified
  • deleted_flag - Soft delete tracking
  • session_id - Links to session (SET NULL on delete)
  • date - When payment actually occurred

Notes

  • Transactions use SET NULL on session deletion (preserves financial records)
  • Artists cannot be deleted if they have transactions (ON DELETE RESTRICT)
  • Always exclude deleted_flag == True in revenue calculations
  • The date field is for the actual payment date, not creation date
  • Use created_at for audit purposes
  • Commission amounts should be calculated and stored at creation time
  • Location in codebase: data/models/transaction.py:19

Build docs developers (and LLMs) love