Point-of-Sale transaction logic for FlashPOS. Covers sale creation and completion, payment processing, returns/refunds with manager approval, inventory stock coordination, sales adjustments, tax calculations (6% SST default), and transaction state management. Use when implementing POS features, handling sale flows, managing returns, or coordinating multi-entity transactions.
Expert guide for Point-of-Sale transaction flows, including sales, returns, adjustments, and inventory coordination. Use when implementing any transaction-related feature.
@Entity(tableName = "sales")
data class Sale(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val subtotalAmount: Double, // Before tax
val taxAmount: Double, // Calculated (subtotal * 0.06)
val totalAmount: Double, // Final amount (subtotal + tax)
val paymentMethod: String, // CASH, CARD, DIGITAL_WALLET
val saleStatus: String, // PENDING, COMPLETED, REFUNDED, CANCELLED
val createdAt: Long = System.currentTimeMillis(),
val completedAt: Long? = null,
val refundedAt: Long? = null,
val notes: String? = null
)
@Entity(
tableName = "sale_items",
foreignKeys = [
ForeignKey(
entity = Sale::class,
parentColumns = ["id"],
childColumns = ["saleId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Product::class,
parentColumns = ["id"],
childColumns = ["productId"],
onDelete = ForeignKey.RESTRICT
)
]
)
data class SaleItem(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val saleId: Int,
val productId: Int,
val productName: String, // Cached for history
val quantity: Int,
val unitPrice: Double,
val totalPrice: Double, // quantity * unitPrice (no tax)
val createdAt: Long = System.currentTimeMillis()
)
// Related data class (join for UI)
data class SaleItemWithProduct(
@Embedded
val saleItem: SaleItem,
@Relation(
parentColumn = "productId",
entityColumn = "id"
)
val product: Product
)
@Entity(
tableName = "returns",
foreignKeys = [
ForeignKey(
entity = Sale::class,
parentColumns = ["id"],
childColumns = ["originalSaleId"],
onDelete = ForeignKey.RESTRICT
)
]
)
data class Return(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val originalSaleId: Int, // Sale being returned
val refundAmount: Double,
val returnReason: String, // DAMAGED, WRONG_ITEM, CUSTOMER_RETURN
val returnStatus: String, // PENDING, APPROVED, REJECTED, COMPLETED
val managerApprovalId: Int? = null, // Manager's user ID
val createdAt: Long = System.currentTimeMillis(),
val approvedAt: Long? = null,
val completedAt: Long? = null
)
@Entity(
tableName = "sale_adjustments",
foreignKeys = [
ForeignKey(
entity = Sale::class,
parentColumns = ["id"],
childColumns = ["saleId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class SaleAdjustment(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val saleId: Int,
val adjustmentType: String, // DISCOUNT, TAX_ADJUSTMENT, CORRECTION
val adjustmentAmount: Double, // Positive or negative
val reason: String,
val appliedBy: Int? = null, // User ID
val createdAt: Long = System.currentTimeMillis()
)
// constants/Constants.kt
object PosConstants {
const val TAX_RATE = 0.06 // 6% SST (Malaysia)
const val CURRENCY_SYMBOL = "RM"
}
// Calculate TAX on SUBTOTAL only
fun calculateTax(subtotal: Double): Double {
return (subtotal * PosConstants.TAX_RATE).roundToTwoDecimals()
}
// Full total calculation
fun calculateTotal(subtotal: Double): Double {
val tax = calculateTax(subtotal)
return (subtotal + tax).roundToTwoDecimals()
}
// Reverse calculation (if user enters total with tax)
fun calculateSubtotalFromTotal(totalWithTax: Double): Double {
val divisor = 1 + PosConstants.TAX_RATE
return (totalWithTax / divisor).roundToTwoDecimals()
}
// Helper extension
fun Double.roundToTwoDecimals(): Double {
return (this * 100).toLong() / 100.0
}
Sale is created AND completed atomically in one database transaction:
// SaleService.kt
class SaleService(
private val saleRepository: SaleRepository,
private val saleItemRepository: SaleItemRepository,
private val productRepository: ProductRepository,
private val database: FlashPosDatabase
) {
suspend fun completeSale(cart: Cart, paymentMethod: String): Result<Sale> {
return try {
var completedSale: Sale? = null
database.withTransaction {
// 1. Create Sale
val subtotal = cart.subtotal()
val tax = cart.totalTax()
val sale = Sale(
subtotalAmount = subtotal,
taxAmount = tax,
totalAmount = subtotal + tax,
paymentMethod = paymentMethod,
saleStatus = "COMPLETED",
completedAt = System.currentTimeMillis()
)
val saleId = saleRepository.saveSale(sale).getOrThrow().toInt()
completedSale = sale.copy(id = saleId)
// 2. Insert SaleItems & update inventory
cart.items.forEach { cartItem ->
val saleItem = SaleItem(
saleId = saleId,
productId = cartItem.product.id,
productName = cartItem.product.name,
quantity = cartItem.quantity,
unitPrice = cartItem.product.price,
totalPrice = cartItem.quantity * cartItem.product.price
)
saleItemRepository.saveSaleItem(saleItem).getOrThrow()
// Decrease stock
productRepository.decreaseStock(
cartItem.product.id,
cartItem.quantity
).getOrThrow()
}
}
Result.success(completedSale!!)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// SaleValidator.kt
class SaleValidator(private val productRepository: ProductRepository) {
fun validateCart(cart: Cart): Result<Unit> {
return when {
cart.items.isEmpty() -> {
Result.failure(Exception("Cart is empty"))
}
cart.totalAmount <= 0 -> {
Result.failure(Exception("Invalid total amount"))
}
cart.items.any { it.quantity <= 0 } -> {
Result.failure(Exception("Invalid quantity in cart"))
}
else -> Result.success(Unit)
}
}
suspend fun validateStockAvailability(cart: Cart): Result<Unit> {
return try {
cart.items.forEach { cartItem ->
val available = productRepository.getStockLevel(cartItem.product.id)
.getOrThrow()
if (available < cartItem.quantity) {
return Result.failure(
Exception("Insufficient stock for ${cartItem.product.name}")
)
}
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
fun validatePaymentMethod(method: String): Result<Unit> {
return if (isValidPaymentMethod(method)) {
Result.success(Unit)
} else {
Result.failure(Exception("Invalid payment method"))
}
}
private fun isValidPaymentMethod(method: String): Boolean {
return method in listOf("CASH", "CARD", "DIGITAL_WALLET")
}
}
// ReturnService.kt
class ReturnService(
private val returnRepository: ReturnRepository,
private val saleRepository: SaleRepository,
private val productRepository: ProductRepository
) {
suspend fun initiateReturn(
originalSaleId: Int,
reason: String
): Result<Return> {
return try {
val sale = saleRepository.getSaleById(originalSaleId)
.getOrNull()
?: return Result.failure(Exception("Sale not found"))
val return_ = Return(
originalSaleId = originalSaleId,
refundAmount = sale.totalAmount,
returnReason = reason,
returnStatus = "PENDING"
)
val returnId = returnRepository.save(return_).getOrThrow().toInt()
Result.success(return_.copy(id = returnId))
} catch (e: Exception) {
Result.failure(e)
}
}
}
// ReturnService.kt (continued)
class ReturnService(...) {
suspend fun approveAndCompleteReturn(
returnId: Int,
managerUserId: Int
): Result<Unit> {
return try {
database.withTransaction {
val return_ = returnRepository.getReturnById(returnId)
.getOrNull()
?: return Result.failure(Exception("Return not found"))
// 1. Update return status
val approvedReturn = return_.copy(
returnStatus = "APPROVED",
managerApprovalId = managerUserId,
approvedAt = System.currentTimeMillis(),
completedAt = System.currentTimeMillis()
)
returnRepository.update(approvedReturn).getOrThrow()
// 2. Update original sale to REFUNDED
val originalSale = saleRepository.getSaleById(return_.originalSaleId)
.getOrThrow()
val refundedSale = originalSale.copy(
saleStatus = "REFUNDED",
refundedAt = System.currentTimeMillis()
)
saleRepository.update(refundedSale).getOrThrow()
// 3. Restore inventory (re-add items to stock)
val saleItems = saleItemRepository.getSaleItemsBySaleId(
return_.originalSaleId
).getOrThrow()
saleItems.forEach { item ->
productRepository.increaseStock(item.productId, item.quantity)
.getOrThrow()
}
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun rejectReturn(returnId: Int, reason: String): Result<Unit> {
return try {
val return_ = returnRepository.getReturnById(returnId)
.getOrNull()
?: return Result.failure(Exception("Return not found"))
val rejectedReturn = return_.copy(
returnStatus = "REJECTED"
)
returnRepository.update(rejectedReturn).getOrThrow()
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
@Entity(tableName = "products")
data class Product(
@PrimaryKey
val id: Int,
val name: String,
val sku: String,
val price: Double,
val stockLevel: Int = 0, // Current stock
val reorderLevel: Int = 10, // Alert when below
val maxStock: Int = 1000
)
// ProductRepository.kt
class ProductRepository(private val productDao: ProductDao) {
suspend fun decreaseStock(productId: Int, quantity: Int): Result<Unit> {
return try {
val product = productDao.getProductById(productId)
?: return Result.failure(Exception("Product not found"))
if (product.stockLevel < quantity) {
return Result.failure(Exception("Insufficient stock"))
}
val updated = product.copy(stockLevel = product.stockLevel - quantity)
productDao.update(updated)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun increaseStock(productId: Int, quantity: Int): Result<Unit> {
return try {
val product = productDao.getProductById(productId)
?: return Result.failure(Exception("Product not found"))
val newStock = product.stockLevel + quantity
if (newStock > product.maxStock) {
return Result.failure(Exception("Stock exceeds maximum"))
}
val updated = product.copy(stockLevel = newStock)
productDao.update(updated)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun getStockLevel(productId: Int): Result<Int> {
return try {
val stock = productDao.getProductById(productId)?.stockLevel
?: return Result.failure(Exception("Product not found"))
Result.success(stock)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// InventoryService.kt
class InventoryService(private val productRepository: ProductRepository) {
suspend fun getLowStockProducts(): Result<List<Product>> {
return try {
val allProducts = productRepository.getAllProducts().getOrThrow()
val lowStock = allProducts.filter {
it.stockLevel <= it.reorderLevel
}
Result.success(lowStock)
} catch (e: Exception) {
Result.failure(e)
}
}
}
// SaleAdjustmentService.kt
class SaleAdjustmentService(
private val adjustmentRepository: SaleAdjustmentRepository,
private val saleRepository: SaleRepository
) {
suspend fun applyDiscount(
saleId: Int,
discountPercentage: Double,
reason: String = "Customer discount"
): Result<Double> {
return try {
val sale = saleRepository.getSaleById(saleId)
.getOrThrow()
val discountAmount = (sale.totalAmount * discountPercentage / 100)
.coerceAtLeast(0.0)
val adjustment = SaleAdjustment(
saleId = saleId,
adjustmentType = "DISCOUNT",
adjustmentAmount = -discountAmount, // Negative = reduction
reason = reason
)
adjustmentRepository.save(adjustment).getOrThrow()
Result.success(discountAmount)
} catch (e: Exception) {
Result.failure(e)
}
}
}
SALE LIFECYCLE:
┌─────────────┐
│ PENDING │ (Sale created, awaiting payment)
└──────┬──────┘
│ Payment confirmed
↓
┌─────────────┐
│ COMPLETED │ (Sale finalized, inventory updated)
└──────┬──────┘
│ Return initiated
↓
┌─────────────┐ (Return pending manager approval)
│ PENDING │
│ (RETURN) │
└──────┬──────┘
│ Manager approves
↓
┌─────────────┐
│ REFUNDED │ (Refund issued, inventory restored)
└─────────────┘
// ViewModel orchestrates the flow
class PosViewModel(
private val saleService: SaleService,
private val returnService: ReturnService,
private val saleValidator: SaleValidator
) : ViewModel() {
fun completeSaleFromCart() {
viewModelScope.launch {
// Step 1: Validate
saleValidator.validateCart(cart).onFailure { error ->
_uiState.value = UiState.Error(error.message)
return@launch
}
// Step 2: Check stock
saleValidator.validateStockAvailability(cart).onFailure { error ->
_uiState.value = UiState.Error(error.message)
return@launch
}
// Step 3: Complete sale
saleService.completeSale(cart, selectedPaymentMethod)
.onSuccess { sale ->
_saleId.value = sale.id
_uiState.value = UiState.SaleCompleted(sale)
clearCart()
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message)
}
}
}
fun initiateReturn(saleId: Int, reason: String) {
viewModelScope.launch {
returnService.initiateReturn(saleId, reason)
.onSuccess { return_ ->
_returnId.value = return_.id
_uiState.value = UiState.ReturnInitiated(return_)
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message)
}
}
}
fun approveReturn(returnId: Int) {
viewModelScope.launch {
val managerId = getCurrentManager().id
returnService.approveAndCompleteReturn(returnId, managerId)
.onSuccess {
_uiState.value = UiState.ReturnApproved
}
.onFailure { error ->
_uiState.value = UiState.Error(error.message)
}
}
}
}
✓ Implementing sales transaction features
✓ Designing return/refund workflows
✓ Managing inventory stock coordination
✓ Handling multi-entity database transactions
✓ Calculating taxes and adjustments
✓ Coordinating manager approval flows
✓ Debugging transaction state issues
✗ General database operations (use Room skill)
✗ UI/Composable design (use Layer B patterns)