Transactions Domain - Serializers Documentation
Complete documentation of Transactions domain API serialization layer.
Source: api/nextango/apps/transactions/serializers.py
Last Updated: 2026-07-08
Table of Contents
Overview
The Transactions serializers handle API input/output for sales, returns, payments, and cash drawer operations.
Serializer Categories:
- Model Serializers: SaleTransactionSerializer, ReturnSerializer, StoreCreditSerializer, and related model-backed serializers
- Action Serializers: Transaction creation, line item operations, payment processing, cash drawer operations
Key Patterns:
- Read/Write Separation: Separate serializers for read (ModelSerializer) vs write (Serializer) operations
- Nested Serialization: Payment records, return records, line items nested in transactions
- Security: Financial totals computed by backend, not accepted from frontend
- Validation: Financial validators, input sanitizers, enum choice validation
- Circular Import Protection: Late imports to avoid circular dependencies
Model Serializers
SaleLineItemWriteSerializer
Purpose: Write-only serializer for creating line items during transaction creation.
Source: serializers.py:20-38
Class Definition:
class SaleLineItemWriteSerializer(serializers.Serializer):
"""
Write-only serializer for creating line items during transaction creation.
Uses plain Serializer to avoid ModelSerializer's ForeignKey resolution issues.
"""
Pattern: Uses plain Serializer (not ModelSerializer) to avoid ForeignKey resolution issues during transaction creation.
Fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
productVariant | UUIDField | Yes | - | Product variant UUID |
quantity | IntegerField | Yes | - | Quantity (min: 1) |
unitPrice | DecimalField(10,2) | No | null | Unit price (optional, fetched from product) |
lineDiscount | DecimalField(10,2) | No | 0 | Line-level discount |
Source: serializers.py:25-38
SaleLineItemSerializer
Purpose: Serializer for SaleLineItem model (read operations).
Source: serializers.py:40-101
Class Definition:
class SaleLineItemSerializer(serializers.ModelSerializer):
"""Serializer for SaleLineItem model (read operations)"""
Fields
| Field | Type | Source | Read Only | Description |
|---|---|---|---|---|
id | UUID | - | Yes | Line item ID |
transaction | PrimaryKeyRelatedField | - | Yes | Parent transaction |
transaction_id | CharField | transaction.transactionId | Yes | Transaction ID string |
productVariant | PrimaryKeyRelatedField | - | No | Product variant reference |
quantity | PositiveIntegerField | - | No | Quantity purchased |
unitPrice | DecimalField(10,2) | - | No | Unit price at sale time |
totalPrice | DecimalField(10,2) | - | Yes | Total price (auto-calculated) |
lineDiscount | DecimalField(10,2) | - | No | Line discount (default: 0) |
sku | SerializerMethodField | get_sku() | Yes | SKU (from snapshot or variant) |
name | SerializerMethodField | get_name() | Yes | Name (from snapshot or variant) |
product_name | SerializerMethodField | get_product_name() | Yes | Product name |
variant_name | SerializerMethodField | get_variant_name() | Yes | Variant name |
skuSnapshot | TextField | - | Yes | SKU snapshot at sale time |
nameSnapshot | TextField | - | Yes | Name snapshot at sale time |
created_at | DateTimeField | - | Yes | Creation timestamp |
updated_at | DateTimeField | - | Yes | Update timestamp |
Source: serializers.py:42-101
Computed Fields
get_sku() Method
Source: serializers.py:61-67
def get_sku(self, obj):
"""Get SKU from snapshot, fallback to productVariant if empty"""
if obj.skuSnapshot:
return obj.skuSnapshot
if obj.productVariant:
return obj.productVariant.sku or ''
return ''
Behavior: Prioritizes immutable snapshot, falls back to current product data.
get_name() Method
Source: serializers.py:69-75
def get_name(self, obj):
"""Get name from snapshot, fallback to productVariant if empty"""
if obj.nameSnapshot:
return obj.nameSnapshot
if obj.productVariant:
return obj.productVariant.name or ''
return ''
Behavior: Same priority pattern as SKU.
get_product_name() Method
Source: serializers.py:77-81
def get_product_name(self, obj):
"""Get product name from productVariant"""
if obj.productVariant and obj.productVariant.product:
return obj.productVariant.product.name or ''
return ''
Behavior: Navigates productVariant → product → name.
get_variant_name() Method
Source: serializers.py:83-87
def get_variant_name(self, obj):
"""Get variant name from productVariant"""
if obj.productVariant:
return obj.productVariant.name or ''
return ''
PaymentProcessorSerializer
Purpose: Serializer for payment processor configuration.
Source: serializers.py:103-143
Class Definition:
class PaymentProcessorSerializer(serializers.ModelSerializer):
Fields
| Field | Type | Constraints | Description |
|---|---|---|---|
id | IntegerField | read_only | Processor ID |
name | CharField(100) | validated | Processor name |
processor_type | CharField | choices | Processor type (stripe, square, paypal, custom) |
is_active | BooleanField | - | Whether processor is active |
is_test_mode | BooleanField | - | Test mode flag |
default_currency | CharField | - | Default currency code |
processing_fee_percentage | DecimalField | 0-10 range | Fee percentage (0-10%) |
processing_fee_fixed | DecimalField(10,2) | $0-$100 range | Fixed fee amount |
created_at | DateTimeField | read_only | Creation timestamp |
updated_at | DateTimeField | read_only | Update timestamp |
sanity_id | CharField | read_only | Sanity CMS ID |
last_synced_at | DateTimeField | read_only | Last sync timestamp |
sync_enabled | BooleanField | read_only | Sync enabled flag |
Source: serializers.py:104-113
Validation
Processor Type Validation
Source: serializers.py:122-125
def validate_processor_type(self, value):
"""Validate processor type"""
valid_types = ['stripe', 'square', 'paypal', 'custom']
return validate_enum_choice(value, valid_types, "processor type")
Valid Values: stripe, square, paypal, custom
Processing Fee Percentage Validation
Source: serializers.py:127-131
def validate_processing_fee_percentage(self, value):
"""Validate processing fee percentage"""
if value < 0 or value > 10: # 0% to 10% reasonable range
raise serializers.ValidationError("Processing fee percentage must be between 0 and 10")
return value
Range: 0% to 10% (reasonable business range)
Fixed Fee Validation
Source: serializers.py:133-143
def validate_processing_fee_fixed(self, value):
"""Validate fixed processing fee"""
try:
return FinancialValidator.validate_amount(
value,
min_amount=Decimal('0.00'),
max_amount=Decimal('100.00'),
field_name="processing fee"
)
except DjangoValidationError as e:
raise serializers.ValidationError(str(e))
Range: $0.00 to $100.00
PaymentMethodSerializer
Purpose: Serializer for payment method configuration.
Source: serializers.py:145-231
Class Definition:
class PaymentMethodSerializer(serializers.ModelSerializer):
Fields
| Field | Type | Constraints | Description |
|---|---|---|---|
id | IntegerField | read_only | Payment method ID |
name | CharField(100) | validated | Method name |
method_type | CharField | choices | Method type (cash, credit_card, etc.) |
processor | Nested | read_only | Payment processor (nested) |
processor_id | IntegerField | write_only | Processor ID (for creation) |
is_active | BooleanField | - | Active flag |
is_online_available | BooleanField | - | Available online |
is_pos_available | BooleanField | - | Available at POS |
minimum_amount | DecimalField(10,2) | $0.01-$1,000 | Minimum transaction amount |
maximum_amount | DecimalField(10,2) | null or $0.01-$1,000,000 | Maximum transaction amount |
configuration | JSONField | validated keys | Method configuration |
display_order | IntegerField | - | Display order in UI |
icon_url | URLField | - | Icon URL |
description | CharField(500) | sanitized | Method description |
sanity_id, last_synced_at, sync_enabled | Sync fields | read_only | Sanity sync fields |
Source: serializers.py:149-159
Validation
Method Type Validation
Source: serializers.py:168-171
def validate_method_type(self, value):
"""Validate method type"""
valid_types = ['cash', 'credit_card', 'debit_card', 'digital_wallet', 'store_credit', 'gift_card', 'other']
return validate_enum_choice(value, valid_types, "method type")
Valid Values: 7 payment method types
Configuration JSON Validation
Source: serializers.py:199-212
def validate_configuration(self, value):
"""Validate configuration JSON"""
if not value:
return {}
allowed_config_keys = [
'terminal_id', 'merchant_id', 'api_version', 'webhook_url',
'require_cvv', 'require_signature', 'timeout_seconds'
]
try:
return validate_json_data(value, allowed_config_keys, "configuration")
except DjangoValidationError as e:
raise serializers.ValidationError(str(e))
Allowed Keys: 7 specific configuration keys for security.
Cross-Field Validation
Source: serializers.py:223-231
def validate(self, attrs):
"""Cross-field validation"""
minimum_amount = attrs.get('minimum_amount')
maximum_amount = attrs.get('maximum_amount')
if minimum_amount and maximum_amount and minimum_amount >= maximum_amount:
raise serializers.ValidationError("Minimum amount must be less than maximum amount")
return attrs
Business Rule: Minimum amount must be less than maximum amount.
TransactionPaymentSerializer
Purpose: Serializer for payment records.
Source: serializers.py:233-268
Class Definition:
class TransactionPaymentSerializer(serializers.ModelSerializer):
Fields
| Field | Type | Access | Description |
|---|---|---|---|
id | IntegerField | read_only | Payment ID |
transaction | ForeignKey | write | Transaction reference |
payment_method | Nested | read_only | Payment method (nested) |
payment_method_id | IntegerField | write_only | Payment method ID |
amount | DecimalField(10,2) | write | Payment amount ($0.01-$100,000) |
status | CharField | write | Payment status (succeeded, pending, failed) |
processor_transaction_id | CharField | read_only | External processor transaction ID |
processor_response | JSONField | read_only | Full processor response |
processing_fee | DecimalField(10,2) | read_only | Processing fee charged |
net_amount | DecimalField(10,2) | read_only | Net amount after fees |
risk_score | DecimalField(5,2) | read_only | Fraud risk score |
fraud_detection_result | JSONField | read_only | Fraud detection details |
created_at | DateTimeField | read_only | Creation timestamp |
updated_at | DateTimeField | read_only | Update timestamp |
processed_at | DateTimeField | read_only | Processing timestamp |
metadata | JSONField | - | Additional metadata |
cash_drawer_event | ForeignKey | read_only | Associated cash drawer event |
| Sync fields | - | read_only | Sanity sync fields |
Source: serializers.py:236-251
Validation
Amount Validation
Source: serializers.py:253-263
def validate_amount(self, value):
"""Validate payment amount"""
try:
return FinancialValidator.validate_amount(
value,
min_amount=Decimal('0.01'),
max_amount=Decimal('100000.00'),
field_name="payment amount"
)
except DjangoValidationError as e:
raise serializers.ValidationError(str(e))
Range: $0.01 to $100,000.00
SaleTransactionSerializer
Purpose: Serializer for SaleTransaction with nested line items and computed properties.
Source: serializers.py:270-352
Class Definition:
class SaleTransactionSerializer(serializers.ModelSerializer):
"""
Serializer for SaleTransaction with JSONField line_items.
Matches Sanity salesTransaction schema structure.
"""
Fields
| Field | Type | Source/Computed | Description |
|---|---|---|---|
id | UUID | - | Transaction UUID |
transactionId | CharField(100) | read_only | Transaction ID string |
store | ForeignKey | - | Store reference |
store_id | UUIDField | store.id | Store UUID (read_only) |
employee | ForeignKey | - | Employee reference |
employee_id | UUIDField | employee.id | Employee UUID (read_only) |
employee_name | CharField | employee.name | Employee name (read_only) |
customer | ForeignKey | - | Customer reference |
customer_id | UUIDField | customer.id | Customer UUID (read_only) |
customer_name | CharField | customer.user.name | Customer name (read_only) |
line_items | Nested | many=True | Line items (nested) |
payment_records | SerializerMethodField | get_payment_records() | Payment records (computed) |
return_records | SerializerMethodField | get_return_records() | Return records (computed) |
pickup_schedule | SerializerMethodField | get_pickup_schedule() | Pickup schedule for pickup fulfillment orders, times converted to store timezone; null for non-pickup orders or when no schedule exists (read_only) |
transaction_type | SerializerMethodField | get_transaction_type() | Compliance tier read from the active vertical's transaction compliance profile, exposed for backward API compatibility (read_only). Not a stored field on SaleTransaction: see Models. |
subtotal | DecimalField(10,2) | read_only | Subtotal before discounts |
taxAmount | DecimalField(10,2) | read_only | Tax amount |
totalAmount | DecimalField(10,2) | read_only | Total amount |
orderLevelDiscount | DecimalField(10,2) | read_only | Promotional discount |
appliedPromoCodes | ManyToManyField | - | Applied promo codes |
sourceCheckoutId | TextField | - | Source cart/checkout ID |
fulfillmentMethod | CharField(20) | - | Fulfillment method (pickup, ship, etc.) |
payment_timing | CharField(20) | - | When payment is collected (immediate or at_pickup) |
status | CharField(30) | - | Transaction status |
timestamp | DateTimeField | read_only | Creation timestamp |
completedAt | DateTimeField | read_only | Completion timestamp |
voidedAt | DateTimeField | read_only | Void timestamp |
voidReason | TextField | - | Void reason |
notes | TextField | blank=True, default="" | Internal staff notes; writable on POST/PUT/PATCH, returned in all GET responses; optional, never null |
payment_balance | DecimalField(10,2) | property | Remaining balance to pay |
is_fully_paid | BooleanField | property | Whether fully paid |
get_refundable_amount | DecimalField(10,2) | property | Refundable amount |
can_be_returned | BooleanField | property | Whether can be returned |
| Audit fields | - | read_only | created_at, updated_at, created_by, updated_by |
| Sync fields | - | read_only | sanity_id, last_synced_at, sync_enabled |
Source: serializers.py:270-330
get_transaction_type() delegates to resolve_transaction_type() in nextango.apps.compliance.services.transaction_compliance, imported lazily to avoid a circular import (compliance's cannabis package holds an FK back into transactions).
get_pickup_schedule() returns None immediately when fulfillmentMethod != 'pickup' or when the transaction has no linked fulfillment; otherwise it looks up the associated PickupSchedule and converts its scheduled time into the store's timezone.
Computed Fields
get_payment_records() Method
Source: serializers.py:290-293
def get_payment_records(self, obj):
"""Serialize payment records - import here to avoid circular import"""
from nextango.apps.payments.serializers import TransactionPaymentSerializer
return TransactionPaymentSerializer(obj.payment_records.all(), many=True).data
Pattern: Late import to avoid circular dependencies.
get_return_records() Method
Source: serializers.py:295-301
def get_return_records(self, obj):
"""Serialize return records - provides complete transaction lifecycle"""
from .models import Return
returns = Return.objects.filter(
originalSaleId=obj
).prefetch_related('return_line_items', 'refunds')
return ReturnSerializer(returns, many=True).data
Optimization: Uses prefetch_related() to avoid N+1 queries.
Validation
Status Validation
Source: serializers.py:382-385
def validate_status(self, value):
"""Validate transaction status"""
valid_statuses = ['pending', 'partially_paid', 'completed', 'voided', 'refunded', 'partially_refunded', 'cancelled']
return validate_enum_choice(value, valid_statuses, "transaction status")
Valid Values: These 7 transaction states are the ones a client may set directly: pending, partially_paid, completed, voided, refunded, partially_refunded, cancelled. The model's eighth state, payment_requires_action (see Models), is not in this list: it is set internally during 3D Secure / SCA card-authentication flows and is not a client-writable status transition.
Client note: Clients filtering or updating status via this field must pass one of the 7 values above; any other value raises a 400 validation error. Clients reading transaction lists must still account for
payment_requires_actionappearing as a read value, even though it cannot be written here.
Fulfillment Method Validation
Source: serializers.py:330-333
def validate_fulfillmentMethod(self, value):
"""Validate fulfillment method"""
valid_methods = ['pickup', 'ship', 'digital', 'counter_sale']
return validate_enum_choice(value, valid_methods, "fulfillment method")
Valid Values: 4 fulfillment types
Line Items Validation
Source: serializers.py:335-343
def validate_line_items(self, value):
"""Validate line items array"""
if not value:
raise serializers.ValidationError("Transaction must have at least one line item")
if len(value) > 100:
raise serializers.ValidationError("Transaction cannot have more than 100 line items")
return value
Business Rules:
- Minimum: 1 line item
- Maximum: 100 line items
ReturnLineItemSerializer
Purpose: Serializer for ReturnLineItem model.
Source: serializers.py:354-375
Class Definition:
class ReturnLineItemSerializer(serializers.ModelSerializer):
"""Serializer for ReturnLineItem model"""
Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Return line item ID |
sales_return | ForeignKey | Parent return |
original_sale_line_item | ForeignKey | Original line item being returned |
nameSnapshot | TextField | Product name at return time (read_only) |
skuSnapshot | TextField | SKU at return time (read_only) |
original_line_item_sku | CharField | Alias for skuSnapshot (read_only) |
original_line_item_name | CharField | Alias for nameSnapshot (read_only) |
quantity | PositiveIntegerField | Quantity being returned |
returnPrice | DecimalField(10,2) | Value of item (read_only) |
reason | CharField(20) | Return reason (choices) |
disposition | CharField(20) | Item disposition (choices) |
reference_inventory_levels | ManyToManyField | Associated inventory levels |
| Timestamps | DateTimeField | created_at, updated_at (read_only) |
| Sync fields | - | sanity_id, last_synced_at, sync_enabled (read_only) |
Source: serializers.py:356-375
Aliases: original_line_item_sku and original_line_item_name provide alternative field names for API consumers.
ReturnSerializer
Purpose: Serializer for Return model with refund-first architecture.
Source: serializers.py:377-429
Class Definition:
class ReturnSerializer(serializers.ModelSerializer):
"""Serializer for Return model with refund-first architecture"""
Fields
| Field | Type | Source/Computed | Description |
|---|---|---|---|
id | UUID | - | Return UUID |
returnId | CharField(100) | read_only | Return ID string |
originalSaleId | ForeignKey | read_only | Original transaction |
original_transaction_id | CharField | originalSaleId.transactionId | Original transaction ID (read_only) |
employeeId | ForeignKey | - | Employee who initiated return |
employeeId_name | CharField | employeeId.name | Employee name (read_only) |
processedBy | ForeignKey | - | Employee who processed return |
processedBy_name | CharField | processedBy.name | Processor name (read_only) |
customer_name | SerializerMethodField | get_customer_name() | Customer name (computed) |
reason | TextField | - | Return reason |
status | CharField(20) | - | Return status |
processedAt | DateTimeField | read_only | Processing timestamp |
return_line_items | Nested | many=True | Return line items (nested) |
refunds | SerializerMethodField | get_refunds() | Refund records (computed) |
total_return_value | SerializerMethodField | get_total_return_value() | Total value (computed) |
| Timestamps | DateTimeField | created_at, updated_at (read_only) | |
| Sync fields | - | sanity_id, last_synced_at, sync_enabled (read_only) |
Source: serializers.py:379-420
Computed Fields
get_customer_name() Method
Source: serializers.py:388-392
def get_customer_name(self, obj):
"""Get customer name from original transaction"""
if obj.customer and hasattr(obj.customer, 'user'):
return obj.customer.user.name
return None
get_total_return_value() Method
Source: serializers.py:394-396
def get_total_return_value(self, obj):
"""Get total return value"""
return obj.get_total_return_value()
Behavior: Delegates to model method.
get_refunds() Method
Source: serializers.py:398-401
def get_refunds(self, obj):
"""Serialize refunds - import here to avoid circular import"""
from nextango.apps.payments.serializers import RefundSerializer
return RefundSerializer(obj.refunds.all(), many=True).data
Pattern: Late import to avoid circular dependencies.
StoreCreditSerializer
Purpose: Serializer for StoreCredit model.
Source: serializers.py:431-471
Class Definition:
class StoreCreditSerializer(serializers.ModelSerializer):
Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Store credit UUID |
creditId | CharField(100) | Credit ID string (read_only) |
customerId | ForeignKey | Customer who owns credit |
customer_name | CharField | Customer name (read_only, from customer.user.name) |
initialAmount | DecimalField(10,2) | Original credit amount |
remainingBalance | DecimalField(10,2) | Current balance |
expirationDate | DateField | Expiration date |
isActive | BooleanField | Active flag |
is_usable | BooleanField | Computed usability (read_only) |
sourceReturn | ForeignKey | Return that generated credit |
| Audit fields | DateTimeField | created_at, updated_at, created_by, updated_by (read_only) |
| Sync fields | - | sanity_id, last_synced_at, sync_enabled (read_only) |
Source: serializers.py:433-447
Validation
Amount Validation
Source: serializers.py:449-459
def validate_amount(self, value):
"""Validate credit amount"""
try:
return FinancialValidator.validate_amount(
value,
min_amount=Decimal('0.01'),
max_amount=Decimal('10000.00'),
field_name="credit amount"
)
except DjangoValidationError as e:
raise serializers.ValidationError(str(e))
Range: $0.01 to $10,000.00
Remaining Balance Validation
Source: serializers.py:461-471
def validate_remaining_balance(self, value):
"""Validate remaining balance"""
try:
return FinancialValidator.validate_amount(
value,
min_amount=Decimal('0.00'),
max_amount=Decimal('10000.00'),
field_name="remaining balance"
)
except DjangoValidationError as e:
raise serializers.ValidationError(str(e))
Range: $0.00 to $10,000.00 (allows zero for depleted credits)
CashDrawerEventSerializer
Purpose: Serializer for CashDrawerEvent model.
Source: serializers.py:473-508
Class Definition:
class CashDrawerEventSerializer(serializers.ModelSerializer):
Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Event UUID |
eventId | CharField(100) | Event ID string (read_only) |
eventType | CharField(20) | Event type (choices) |
store | ForeignKey | Store reference |
store_name | CharField | Store name (read_only, from store.name) |
employee | ForeignKey | Employee who triggered event |
employee_name | CharField | Employee name (read_only, from employee.user.name) |
registerId | CharField(50) | POS register identifier |
shiftId | CharField(100) | Employee shift identifier |
amount | DecimalField(10,2) | Cash amount involved |
drawerTotalBefore | DecimalField(10,2) | Drawer total before event |
drawerTotalAfter | DecimalField(10,2) | Drawer total after event |
transaction | ForeignKey | Associated transaction |
cashBreakdown | JSONField | Cash denomination breakdown |
reason | CharField(255) | Reason for event |
notes | TextField | Additional notes |
timestamp | DateTimeField | Event timestamp (read_only) |
| Audit fields | DateTimeField | created_at, updated_at, created_by, updated_by (read_only) |
| Sync fields | - | sanity_id, last_synced_at, sync_enabled (read_only) |
Source: serializers.py:475-491
Validation
Event Type Validation
Source: serializers.py:493-496
def validate_event_type(self, value):
"""Validate event type"""
valid_types = ['open', 'close', 'sale', 'return', 'cash_in', 'cash_out', 'deposit', 'till_count', 'no_sale']
return validate_enum_choice(value, valid_types, "event type")
Valid Values: 9 cash drawer event types
Amount Validation
Source: serializers.py:498-508
def validate_amount(self, value):
"""Validate cash amount"""
try:
return FinancialValidator.validate_amount(
value,
min_amount=Decimal('-10000.00'), # Allow negative for cash out
max_amount=Decimal('10000.00'),
field_name="cash amount"
)
except DjangoValidationError as e:
raise serializers.ValidationError(str(e))
Range: -$10,000.00 to $10,000.00 (negative allowed for cash out operations)
CartSerializer
Purpose: Serializer for Cart model.
Source: serializers.py:629-650
Class Definition:
class CartSerializer(serializers.ModelSerializer):
Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Cart UUID |
cartId | UUIDField | Cart ID (read_only) |
customer | ForeignKey | Customer who owns cart |
customer_name | CharField | Customer name (read_only, from customer.user.name) |
status | CharField(20) | Cart status (active, merged, completed) |
line_items | JSONField | Line items array |
item_count | IntegerField | Total item count (read_only, computed) |
is_empty | BooleanField | Empty flag (read_only, computed) |
created_at | DateTimeField | Creation timestamp (read_only) |
updated_at | DateTimeField | Update timestamp (read_only) |
| Sync fields | - | sanity_id, last_synced_at, sync_enabled (read_only) |
Source: serializers.py:631-645
Validation
Status Validation
Source: serializers.py:647-650
def validate_status(self, value):
"""Validate cart status"""
valid_statuses = ['active', 'merged', 'completed']
return validate_enum_choice(value, valid_statuses, "cart status")
Valid Values: active, merged, completed
Action Serializers
Action serializers are plain Serializer classes (not ModelSerializer) used for specific API actions.
SaleTransactionCreateSerializer
Purpose: Serializer for creating new sale transactions from POS systems.
Source: serializers.py:510-549
Class Definition:
class SaleTransactionCreateSerializer(serializers.Serializer):
"""
Serializer for creating new sale transactions from POS systems.
SECURITY: Financial fields (subtotal, taxAmount, totalAmount) are IGNORED by backend.
Backend calculates all totals from line items using recalculate_totals().
These fields remain in serializer for backwards compatibility but are not required.
"""
Security Note: Financial totals are computed server-side and NOT accepted from client.
Fields
| Field | Type | Required | Source | Description |
|---|---|---|---|---|
storeId | UUIDField | No | store_id | Store UUID |
employeeId | UUIDField | No | employee_id | Employee UUID |
customerId | UUIDField | No | customer_id | Customer UUID |
line_items | ListField | No | - | Line items array (SaleLineItemWriteSerializer) |
subtotal | DecimalField(10,2) | No | - | IGNORED by backend |
taxAmount | DecimalField(10,2) | No | - | IGNORED by backend |
totalAmount | DecimalField(10,2) | No | - | IGNORED by backend |
status | ChoiceField | No | - | Transaction status (default: pending) |
payment_method | CharField(20) | No | - | Payment method type |
paymentMethodId | UUIDField | No | payment_method_id | Payment method ID |
processorTransactionId | CharField(255) | No | processor_transaction_id | External processor ID |
tendered_amount | DecimalField(10,2) | No | - | Amount tendered by customer |
change_due | DecimalField(10,2) | No | - | Change due to customer |
appliedPromoCodes | ListField | No | applied_promo_codes | Promo code array |
Source: serializers.py:518-549
Important: subtotal, taxAmount, and totalAmount are IGNORED for security. Backend recalculates all financial values.
AddSaleLineItemSerializer
Purpose: Serializer for adding line items to existing transactions.
Source: serializers.py:552-556
class AddSaleLineItemSerializer(serializers.Serializer):
"""Serializer for adding line items to transactions"""
product_variant_id = serializers.UUIDField(required=True)
quantity = serializers.IntegerField(min_value=1, max_value=1000, default=1)
unit_price = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
Fields:
product_variant_id: Product variant UUID (required)quantity: Quantity (1-1,000, default: 1)unit_price: Unit price (optional, fetched from product if not provided)
RemoveSaleLineItemSerializer
Purpose: Serializer for removing line items from transactions.
Source: serializers.py:559-561
class RemoveSaleLineItemSerializer(serializers.Serializer):
"""Serializer for removing line items from transactions"""
line_item_id = serializers.UUIDField(required=True)
Fields:
line_item_id: Line item UUID to remove (required)
UpdateSaleLineItemQuantitySerializer
Purpose: Serializer for updating line item quantities.
Source: serializers.py:564-567
class UpdateSaleLineItemQuantitySerializer(serializers.Serializer):
"""Serializer for updating line item quantities"""
line_item_id = serializers.UUIDField(required=True)
quantity = serializers.IntegerField(min_value=1, max_value=1000)
Fields:
line_item_id: Line item UUID (required)quantity: New quantity (1-1,000, required)
ProcessPaymentRequestSerializer
Purpose: Serializer for processing payment requests.
Source: serializers.py:570-574
class ProcessPaymentRequestSerializer(serializers.Serializer):
"""Serializer for processing payment requests"""
payment_method_id = serializers.IntegerField(required=True)
amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
tendered_amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
Fields:
payment_method_id: Payment method ID (required)amount: Payment amount (optional, uses transaction total if not provided)tendered_amount: Amount tendered by customer (optional, for change calculation)
VoidTransactionSerializer
Purpose: Serializer for voiding transactions.
Source: serializers.py:577-580
class VoidTransactionSerializer(serializers.Serializer):
"""Serializer for voiding transactions"""
reason = serializers.CharField(max_length=1000, required=True)
manager_override = serializers.BooleanField(default=False)
Fields:
reason: Void reason (required, max 1,000 characters)manager_override: Whether manager override was used (default: False)
AvailablePaymentMethodsSerializer
Purpose: Serializer for getting available payment methods.
Source: serializers.py:583-586
class AvailablePaymentMethodsSerializer(serializers.Serializer):
"""Serializer for getting available payment methods"""
type = serializers.ChoiceField(choices=[('pos', 'POS'), ('online', 'Online')], default='pos')
amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
Fields:
type: Payment context (pos or online, default: pos)amount: Transaction amount (optional, for filtering by min/max amounts)
OpenDrawerSerializer
Purpose: Serializer for opening cash drawer.
Source: serializers.py:589-594
class OpenDrawerSerializer(serializers.Serializer):
"""Serializer for opening cash drawer"""
store_id = serializers.UUIDField(required=True)
register_id = serializers.CharField(max_length=50, required=True)
shift_id = serializers.CharField(max_length=100, required=False)
starting_amount = serializers.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
Fields:
store_id: Store UUID (required)register_id: POS register ID (required)shift_id: Employee shift ID (optional)starting_amount: Starting cash amount (default: $0.00)
CloseDrawerSerializer
Purpose: Serializer for closing cash drawer.
Source: serializers.py:597-603
class CloseDrawerSerializer(serializers.Serializer):
"""Serializer for closing cash drawer"""
store_id = serializers.UUIDField(required=True)
register_id = serializers.CharField(max_length=50, required=True)
shift_id = serializers.CharField(max_length=100, required=False)
ending_amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=True)
cash_breakdown = serializers.JSONField(default=dict)
Fields:
store_id: Store UUID (required)register_id: POS register ID (required)shift_id: Employee shift ID (optional)ending_amount: Ending cash amount (required)cash_breakdown: Cash denomination breakdown (optional, default: )
CashOperationSerializer
Purpose: Serializer for cash drawer operations (add/remove cash).
Source: serializers.py:606-612
class CashOperationSerializer(serializers.Serializer):
"""Serializer for cash drawer operations (add/remove cash)"""
store_id = serializers.UUIDField(required=True)
register_id = serializers.CharField(max_length=50, required=True)
amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=True)
reason = serializers.CharField(max_length=255, required=True)
notes = serializers.CharField(max_length=1000, required=False, allow_blank=True)
Fields:
store_id: Store UUID (required)register_id: POS register ID (required)amount: Cash amount (positive for add, negative for remove, required)reason: Operation reason (required, max 255 characters)notes: Additional notes (optional, max 1,000 characters)
CashDrawerStatusSerializer
Purpose: Serializer for getting cash drawer status.
Source: serializers.py:615-618
class CashDrawerStatusSerializer(serializers.Serializer):
"""Serializer for getting cash drawer status"""
store_id = serializers.UUIDField(required=True)
register_id = serializers.CharField(max_length=50, required=True)
Fields:
store_id: Store UUID (required)register_id: POS register ID (required)
TillCountSerializer
Purpose: Serializer for recording till counts.
Source: serializers.py:621-627
class TillCountSerializer(serializers.Serializer):
"""Serializer for recording till counts"""
store_id = serializers.UUIDField(required=True)
register_id = serializers.CharField(max_length=50, required=True)
counted_amount = serializers.DecimalField(max_digits=10, decimal_places=2, required=True)
cash_breakdown = serializers.JSONField(default=dict)
notes = serializers.CharField(max_length=1000, required=False, allow_blank=True)
Fields:
store_id: Store UUID (required)register_id: POS register ID (required)counted_amount: Counted cash amount (required)cash_breakdown: Cash denomination breakdown (optional, default: )notes: Additional notes (optional, max 1,000 characters)
Validation Patterns
Financial Validation
Pattern: All monetary fields use FinancialValidator for consistent validation.
Example:
def validate_amount(self, value):
try:
return FinancialValidator.validate_amount(
value,
min_amount=Decimal('0.01'),
max_amount=Decimal('100000.00'),
field_name="payment amount"
)
except DjangoValidationError as e:
raise serializers.ValidationError(str(e))
Validators Used:
FinancialValidator.validate_amount(): Amount range validationInputSanitizer.sanitize_text(): Text field sanitizationvalidate_enum_choice(): Enum/choice validationvalidate_json_data(): JSONField validation with allowed keys
Enum Choice Validation
Pattern: All enum fields use validate_enum_choice() helper.
Example:
def validate_status(self, value):
valid_statuses = ['pending', 'partially_paid', 'completed', 'voided', 'refunded', 'partially_refunded', 'cancelled']
return validate_enum_choice(value, valid_statuses, "transaction status")
Common Enums:
- Transaction Status: pending, partially_paid, completed, voided, refunded, partially_refunded, cancelled
- Payment Status: succeeded, pending, failed
- Return Status: pending, processed, cancelled
- Fulfillment Method: pickup, ship, digital, counter_sale
- Cart Status: active, merged, completed
- Payment Method Type: cash, credit_card, debit_card, digital_wallet, store_credit, gift_card, other
- Processor Type: stripe, square, paypal, custom
- Event Type: open, close, sale, return, cash_in, cash_out, deposit, till_count, no_sale
Cross-Field Validation
Pattern: Use validate() method for cross-field validation.
Example (PaymentMethodSerializer):
def validate(self, attrs):
"""Cross-field validation"""
minimum_amount = attrs.get('minimum_amount')
maximum_amount = attrs.get('maximum_amount')
if minimum_amount and maximum_amount and minimum_amount >= maximum_amount:
raise serializers.ValidationError("Minimum amount must be less than maximum amount")
return attrs
Circular Import Prevention
Pattern: Use late imports in computed field methods.
Example:
def get_payment_records(self, obj):
"""Serialize payment records - import here to avoid circular import"""
from nextango.apps.payments.serializers import TransactionPaymentSerializer
return TransactionPaymentSerializer(obj.payment_records.all(), many=True).data
Why: Transactions and Payments domains have bidirectional relationships. Late imports prevent circular dependency errors.
Read/Write Separation
Pattern: Separate serializers for read vs write operations.
Examples:
- SaleLineItemSerializer (read): Full model serializer with computed fields
- SaleLineItemWriteSerializer (write): Plain serializer for creation
- SaleTransactionSerializer (read): Nested line items, payments, returns
- SaleTransactionCreateSerializer (write): Simplified creation interface
Benefits:
- Different field requirements for read vs write
- Prevents ForeignKey resolution issues during creation
- Allows computed fields on read without affecting write validation
Security: Server-Side Financial Calculations
Critical Pattern: Financial totals are NEVER accepted from client.
Implementation (SaleTransactionCreateSerializer):
# IGNORED: Backend calculates these - frontend should NOT send them
subtotal = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
taxAmount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
totalAmount = serializers.DecimalField(max_digits=10, decimal_places=2, required=False, allow_null=True)
Docstring Warning:
SECURITY: Financial fields (subtotal, taxAmount, totalAmount) are IGNORED by backend.
Backend calculates all totals from line items using recalculate_totals().
These fields remain in serializer for backwards compatibility but are not required.
Backend Processing (in views/services):
# Backend recalculates all totals
transaction.recalculate_totals(commit=True)
Security Benefits:
- Prevents price manipulation attacks
- Ensures tax calculation integrity
- Guarantees promotional discount accuracy
- Maintains audit trail integrity
Related Documentation
- Models - Transaction data models
- Views - Transaction API endpoints
- Services - Transaction business logic
- Signals - Bidirectional sync with Sanity
- Payments Domain - Payment processing integration
- Products Domain - ProductVariant relationships