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

  1. Overview
  2. Model Serializers
  3. Action Serializers
  4. Validation Patterns

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

FieldTypeRequiredDefaultDescription
productVariantUUIDFieldYes-Product variant UUID
quantityIntegerFieldYes-Quantity (min: 1)
unitPriceDecimalField(10,2)NonullUnit price (optional, fetched from product)
lineDiscountDecimalField(10,2)No0Line-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

FieldTypeSourceRead OnlyDescription
idUUID-YesLine item ID
transactionPrimaryKeyRelatedField-YesParent transaction
transaction_idCharFieldtransaction.transactionIdYesTransaction ID string
productVariantPrimaryKeyRelatedField-NoProduct variant reference
quantityPositiveIntegerField-NoQuantity purchased
unitPriceDecimalField(10,2)-NoUnit price at sale time
totalPriceDecimalField(10,2)-YesTotal price (auto-calculated)
lineDiscountDecimalField(10,2)-NoLine discount (default: 0)
skuSerializerMethodFieldget_sku()YesSKU (from snapshot or variant)
nameSerializerMethodFieldget_name()YesName (from snapshot or variant)
product_nameSerializerMethodFieldget_product_name()YesProduct name
variant_nameSerializerMethodFieldget_variant_name()YesVariant name
skuSnapshotTextField-YesSKU snapshot at sale time
nameSnapshotTextField-YesName snapshot at sale time
created_atDateTimeField-YesCreation timestamp
updated_atDateTimeField-YesUpdate 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

FieldTypeConstraintsDescription
idIntegerFieldread_onlyProcessor ID
nameCharField(100)validatedProcessor name
processor_typeCharFieldchoicesProcessor type (stripe, square, paypal, custom)
is_activeBooleanField-Whether processor is active
is_test_modeBooleanField-Test mode flag
default_currencyCharField-Default currency code
processing_fee_percentageDecimalField0-10 rangeFee percentage (0-10%)
processing_fee_fixedDecimalField(10,2)$0-$100 rangeFixed fee amount
created_atDateTimeFieldread_onlyCreation timestamp
updated_atDateTimeFieldread_onlyUpdate timestamp
sanity_idCharFieldread_onlySanity CMS ID
last_synced_atDateTimeFieldread_onlyLast sync timestamp
sync_enabledBooleanFieldread_onlySync 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

FieldTypeConstraintsDescription
idIntegerFieldread_onlyPayment method ID
nameCharField(100)validatedMethod name
method_typeCharFieldchoicesMethod type (cash, credit_card, etc.)
processorNestedread_onlyPayment processor (nested)
processor_idIntegerFieldwrite_onlyProcessor ID (for creation)
is_activeBooleanField-Active flag
is_online_availableBooleanField-Available online
is_pos_availableBooleanField-Available at POS
minimum_amountDecimalField(10,2)$0.01-$1,000Minimum transaction amount
maximum_amountDecimalField(10,2)null or $0.01-$1,000,000Maximum transaction amount
configurationJSONFieldvalidated keysMethod configuration
display_orderIntegerField-Display order in UI
icon_urlURLField-Icon URL
descriptionCharField(500)sanitizedMethod description
sanity_id, last_synced_at, sync_enabledSync fieldsread_onlySanity 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

FieldTypeAccessDescription
idIntegerFieldread_onlyPayment ID
transactionForeignKeywriteTransaction reference
payment_methodNestedread_onlyPayment method (nested)
payment_method_idIntegerFieldwrite_onlyPayment method ID
amountDecimalField(10,2)writePayment amount ($0.01-$100,000)
statusCharFieldwritePayment status (succeeded, pending, failed)
processor_transaction_idCharFieldread_onlyExternal processor transaction ID
processor_responseJSONFieldread_onlyFull processor response
processing_feeDecimalField(10,2)read_onlyProcessing fee charged
net_amountDecimalField(10,2)read_onlyNet amount after fees
risk_scoreDecimalField(5,2)read_onlyFraud risk score
fraud_detection_resultJSONFieldread_onlyFraud detection details
created_atDateTimeFieldread_onlyCreation timestamp
updated_atDateTimeFieldread_onlyUpdate timestamp
processed_atDateTimeFieldread_onlyProcessing timestamp
metadataJSONField-Additional metadata
cash_drawer_eventForeignKeyread_onlyAssociated cash drawer event
Sync fields-read_onlySanity 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

FieldTypeSource/ComputedDescription
idUUID-Transaction UUID
transactionIdCharField(100)read_onlyTransaction ID string
storeForeignKey-Store reference
store_idUUIDFieldstore.idStore UUID (read_only)
employeeForeignKey-Employee reference
employee_idUUIDFieldemployee.idEmployee UUID (read_only)
employee_nameCharFieldemployee.nameEmployee name (read_only)
customerForeignKey-Customer reference
customer_idUUIDFieldcustomer.idCustomer UUID (read_only)
customer_nameCharFieldcustomer.user.nameCustomer name (read_only)
line_itemsNestedmany=TrueLine items (nested)
payment_recordsSerializerMethodFieldget_payment_records()Payment records (computed)
return_recordsSerializerMethodFieldget_return_records()Return records (computed)
pickup_scheduleSerializerMethodFieldget_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_typeSerializerMethodFieldget_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.
subtotalDecimalField(10,2)read_onlySubtotal before discounts
taxAmountDecimalField(10,2)read_onlyTax amount
totalAmountDecimalField(10,2)read_onlyTotal amount
orderLevelDiscountDecimalField(10,2)read_onlyPromotional discount
appliedPromoCodesManyToManyField-Applied promo codes
sourceCheckoutIdTextField-Source cart/checkout ID
fulfillmentMethodCharField(20)-Fulfillment method (pickup, ship, etc.)
payment_timingCharField(20)-When payment is collected (immediate or at_pickup)
statusCharField(30)-Transaction status
timestampDateTimeFieldread_onlyCreation timestamp
completedAtDateTimeFieldread_onlyCompletion timestamp
voidedAtDateTimeFieldread_onlyVoid timestamp
voidReasonTextField-Void reason
notesTextFieldblank=True, default=""Internal staff notes; writable on POST/PUT/PATCH, returned in all GET responses; optional, never null
payment_balanceDecimalField(10,2)propertyRemaining balance to pay
is_fully_paidBooleanFieldpropertyWhether fully paid
get_refundable_amountDecimalField(10,2)propertyRefundable amount
can_be_returnedBooleanFieldpropertyWhether can be returned
Audit fields-read_onlycreated_at, updated_at, created_by, updated_by
Sync fields-read_onlysanity_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_action appearing 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

FieldTypeDescription
idUUIDReturn line item ID
sales_returnForeignKeyParent return
original_sale_line_itemForeignKeyOriginal line item being returned
nameSnapshotTextFieldProduct name at return time (read_only)
skuSnapshotTextFieldSKU at return time (read_only)
original_line_item_skuCharFieldAlias for skuSnapshot (read_only)
original_line_item_nameCharFieldAlias for nameSnapshot (read_only)
quantityPositiveIntegerFieldQuantity being returned
returnPriceDecimalField(10,2)Value of item (read_only)
reasonCharField(20)Return reason (choices)
dispositionCharField(20)Item disposition (choices)
reference_inventory_levelsManyToManyFieldAssociated inventory levels
TimestampsDateTimeFieldcreated_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

FieldTypeSource/ComputedDescription
idUUID-Return UUID
returnIdCharField(100)read_onlyReturn ID string
originalSaleIdForeignKeyread_onlyOriginal transaction
original_transaction_idCharFieldoriginalSaleId.transactionIdOriginal transaction ID (read_only)
employeeIdForeignKey-Employee who initiated return
employeeId_nameCharFieldemployeeId.nameEmployee name (read_only)
processedByForeignKey-Employee who processed return
processedBy_nameCharFieldprocessedBy.nameProcessor name (read_only)
customer_nameSerializerMethodFieldget_customer_name()Customer name (computed)
reasonTextField-Return reason
statusCharField(20)-Return status
processedAtDateTimeFieldread_onlyProcessing timestamp
return_line_itemsNestedmany=TrueReturn line items (nested)
refundsSerializerMethodFieldget_refunds()Refund records (computed)
total_return_valueSerializerMethodFieldget_total_return_value()Total value (computed)
TimestampsDateTimeFieldcreated_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

FieldTypeDescription
idUUIDStore credit UUID
creditIdCharField(100)Credit ID string (read_only)
customerIdForeignKeyCustomer who owns credit
customer_nameCharFieldCustomer name (read_only, from customer.user.name)
initialAmountDecimalField(10,2)Original credit amount
remainingBalanceDecimalField(10,2)Current balance
expirationDateDateFieldExpiration date
isActiveBooleanFieldActive flag
is_usableBooleanFieldComputed usability (read_only)
sourceReturnForeignKeyReturn that generated credit
Audit fieldsDateTimeFieldcreated_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

FieldTypeDescription
idUUIDEvent UUID
eventIdCharField(100)Event ID string (read_only)
eventTypeCharField(20)Event type (choices)
storeForeignKeyStore reference
store_nameCharFieldStore name (read_only, from store.name)
employeeForeignKeyEmployee who triggered event
employee_nameCharFieldEmployee name (read_only, from employee.user.name)
registerIdCharField(50)POS register identifier
shiftIdCharField(100)Employee shift identifier
amountDecimalField(10,2)Cash amount involved
drawerTotalBeforeDecimalField(10,2)Drawer total before event
drawerTotalAfterDecimalField(10,2)Drawer total after event
transactionForeignKeyAssociated transaction
cashBreakdownJSONFieldCash denomination breakdown
reasonCharField(255)Reason for event
notesTextFieldAdditional notes
timestampDateTimeFieldEvent timestamp (read_only)
Audit fieldsDateTimeFieldcreated_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

FieldTypeDescription
idUUIDCart UUID
cartIdUUIDFieldCart ID (read_only)
customerForeignKeyCustomer who owns cart
customer_nameCharFieldCustomer name (read_only, from customer.user.name)
statusCharField(20)Cart status (active, merged, completed)
line_itemsJSONFieldLine items array
item_countIntegerFieldTotal item count (read_only, computed)
is_emptyBooleanFieldEmpty flag (read_only, computed)
created_atDateTimeFieldCreation timestamp (read_only)
updated_atDateTimeFieldUpdate 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

FieldTypeRequiredSourceDescription
storeIdUUIDFieldNostore_idStore UUID
employeeIdUUIDFieldNoemployee_idEmployee UUID
customerIdUUIDFieldNocustomer_idCustomer UUID
line_itemsListFieldNo-Line items array (SaleLineItemWriteSerializer)
subtotalDecimalField(10,2)No-IGNORED by backend
taxAmountDecimalField(10,2)No-IGNORED by backend
totalAmountDecimalField(10,2)No-IGNORED by backend
statusChoiceFieldNo-Transaction status (default: pending)
payment_methodCharField(20)No-Payment method type
paymentMethodIdUUIDFieldNopayment_method_idPayment method ID
processorTransactionIdCharField(255)Noprocessor_transaction_idExternal processor ID
tendered_amountDecimalField(10,2)No-Amount tendered by customer
change_dueDecimalField(10,2)No-Change due to customer
appliedPromoCodesListFieldNoapplied_promo_codesPromo 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 validation
  • InputSanitizer.sanitize_text(): Text field sanitization
  • validate_enum_choice(): Enum/choice validation
  • validate_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

Was this page helpful?