quite a lot of things
This commit is contained in:
158
internal/gst/calculator.go
Normal file
158
internal/gst/calculator.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package gst
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// Rate represents standard GST rates
|
||||
type Rate float64
|
||||
|
||||
const (
|
||||
Rate0 Rate = 0.0
|
||||
Rate5 Rate = 0.05
|
||||
Rate12 Rate = 0.12
|
||||
Rate18 Rate = 0.18
|
||||
Rate28 Rate = 0.28
|
||||
)
|
||||
|
||||
// CustomerType distinguishes between B2B (Wholesale) and B2C (Retail)
|
||||
type CustomerType string
|
||||
|
||||
const (
|
||||
CustomerWholesale CustomerType = "wholesale"
|
||||
CustomerRetail CustomerType = "retail"
|
||||
)
|
||||
|
||||
// Product represents a catalog item
|
||||
type Product struct {
|
||||
SKU string
|
||||
Name string
|
||||
HSNCode string
|
||||
BasePrice float64 // Price before tax
|
||||
WholesalePrice float64 // Discounted price for B2B
|
||||
GSTRate Rate
|
||||
SmallOrderQty int // Minimum quantity threshold
|
||||
SmallOrderFee float64 // Convenience fee when quantity is below threshold
|
||||
Unit string // Unit of measurement (e.g., "pcs", "kg", "box")
|
||||
}
|
||||
|
||||
// LineItem represents a single row in the invoice
|
||||
type LineItem struct {
|
||||
Product Product
|
||||
Quantity int
|
||||
UnitPrice float64 // Actual price applied (wholesale vs retail)
|
||||
TaxableVal float64 // Quantity * UnitPrice
|
||||
CGSTAmount float64
|
||||
SGSTAmount float64
|
||||
IGSTAmount float64
|
||||
TotalAmount float64
|
||||
}
|
||||
|
||||
// Invoice represents the full bill
|
||||
type Invoice struct {
|
||||
LineItems []LineItem
|
||||
SubTotal float64
|
||||
TotalCGST float64
|
||||
TotalSGST float64
|
||||
TotalIGST float64
|
||||
ConvenienceFee float64 // Flat fee for small orders (before tax)
|
||||
ConvenienceFeeTax float64 // GST on convenience fee (18% fixed)
|
||||
GrandTotal float64
|
||||
CustomerType CustomerType
|
||||
IsInterState bool // True if selling to a different state (IGST applies)
|
||||
CompanyDetails string // Multiline company details (displayed above invoice table)
|
||||
BuyerDetails string // Multiline buyer details (displayed above bank details)
|
||||
BuyerName string // Buyer's name
|
||||
BankDetails string // Multiline bank details (displayed at bottom of invoice)
|
||||
}
|
||||
|
||||
// Calculator handles the GST logic
|
||||
type Calculator struct{}
|
||||
|
||||
// NewCalculator creates a new calculator instance
|
||||
func NewCalculator() *Calculator {
|
||||
return &Calculator{}
|
||||
}
|
||||
|
||||
// CalculateLineItem computes taxes for a single line
|
||||
func (c *Calculator) CalculateLineItem(p Product, qty int, custType CustomerType, isInterState bool) LineItem {
|
||||
// Determine price based on customer type
|
||||
price := p.BasePrice
|
||||
if custType == CustomerWholesale {
|
||||
price = p.WholesalePrice
|
||||
}
|
||||
|
||||
taxableVal := price * float64(qty)
|
||||
rate := float64(p.GSTRate)
|
||||
|
||||
var cgst, sgst, igst float64
|
||||
|
||||
if isInterState {
|
||||
igst = taxableVal * rate
|
||||
} else {
|
||||
// Intra-state: Split tax between Center and State
|
||||
halfRate := rate / 2
|
||||
cgst = taxableVal * halfRate
|
||||
sgst = taxableVal * halfRate
|
||||
}
|
||||
|
||||
total := taxableVal + cgst + sgst + igst
|
||||
|
||||
return LineItem{
|
||||
Product: p,
|
||||
Quantity: qty,
|
||||
UnitPrice: price,
|
||||
TaxableVal: round(taxableVal),
|
||||
CGSTAmount: round(cgst),
|
||||
SGSTAmount: round(sgst),
|
||||
IGSTAmount: round(igst),
|
||||
TotalAmount: round(total),
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateInvoice computes totals for the entire invoice
|
||||
func (c *Calculator) CalculateInvoice(items []LineItem, fee float64, isInterState bool) Invoice {
|
||||
inv := Invoice{
|
||||
LineItems: items,
|
||||
ConvenienceFee: fee,
|
||||
IsInterState: isInterState,
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
inv.SubTotal += item.TaxableVal
|
||||
inv.TotalCGST += item.CGSTAmount
|
||||
inv.TotalSGST += item.SGSTAmount
|
||||
inv.TotalIGST += item.IGSTAmount
|
||||
}
|
||||
|
||||
// Convenience fee is taxable at 18% fixed rate
|
||||
if fee > 0 {
|
||||
feeTax := fee * 0.18 // 18% GST on convenience fee
|
||||
inv.ConvenienceFeeTax = round(feeTax)
|
||||
// Add convenience fee to taxable subtotal
|
||||
inv.SubTotal += fee
|
||||
// Add convenience fee tax to appropriate tax fields
|
||||
if isInterState {
|
||||
inv.TotalIGST += inv.ConvenienceFeeTax
|
||||
} else {
|
||||
// Split between CGST and SGST (9% each)
|
||||
inv.TotalCGST += round(feeTax / 2)
|
||||
inv.TotalSGST += round(feeTax / 2)
|
||||
}
|
||||
}
|
||||
|
||||
inv.GrandTotal = inv.SubTotal + inv.TotalCGST + inv.TotalSGST + inv.TotalIGST
|
||||
|
||||
// Rounding final totals
|
||||
inv.SubTotal = round(inv.SubTotal)
|
||||
inv.TotalCGST = round(inv.TotalCGST)
|
||||
inv.TotalSGST = round(inv.TotalSGST)
|
||||
inv.TotalIGST = round(inv.TotalIGST)
|
||||
inv.GrandTotal = round(inv.GrandTotal)
|
||||
|
||||
return inv
|
||||
}
|
||||
|
||||
func round(num float64) float64 {
|
||||
return math.Round(num*100) / 100
|
||||
}
|
||||
122
internal/gst/calculator_test.go
Normal file
122
internal/gst/calculator_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package gst
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCalculateLineItem(t *testing.T) {
|
||||
c := NewCalculator()
|
||||
|
||||
product := Product{
|
||||
SKU: "TEST01",
|
||||
BasePrice: 100,
|
||||
WholesalePrice: 90,
|
||||
GSTRate: Rate18,
|
||||
}
|
||||
|
||||
// Case 1: Retail, Intra-state
|
||||
item := c.CalculateLineItem(product, 2, CustomerRetail, false)
|
||||
if item.UnitPrice != 100 {
|
||||
t.Errorf("Expected UnitPrice 100, got %f", item.UnitPrice)
|
||||
}
|
||||
if item.TaxableVal != 200 {
|
||||
t.Errorf("Expected TaxableVal 200, got %f", item.TaxableVal)
|
||||
}
|
||||
if item.CGSTAmount != 18 { // 9% of 200
|
||||
t.Errorf("Expected CGST 18, got %f", item.CGSTAmount)
|
||||
}
|
||||
if item.IGSTAmount != 0 {
|
||||
t.Errorf("Expected IGST 0, got %f", item.IGSTAmount)
|
||||
}
|
||||
if item.TotalAmount != 236 { // 200 + 18 + 18
|
||||
t.Errorf("Expected Total 236, got %f", item.TotalAmount)
|
||||
}
|
||||
|
||||
// Case 2: Wholesale, Inter-state
|
||||
item = c.CalculateLineItem(product, 10, CustomerWholesale, true)
|
||||
if item.UnitPrice != 90 {
|
||||
t.Errorf("Expected UnitPrice 90, got %f", item.UnitPrice)
|
||||
}
|
||||
if item.TaxableVal != 900 {
|
||||
t.Errorf("Expected TaxableVal 900, got %f", item.TaxableVal)
|
||||
}
|
||||
if item.CGSTAmount != 0 {
|
||||
t.Errorf("Expected CGST 0, got %f", item.CGSTAmount)
|
||||
}
|
||||
if item.IGSTAmount != 162 { // 18% of 900
|
||||
t.Errorf("Expected IGST 162, got %f", item.IGSTAmount)
|
||||
}
|
||||
if item.TotalAmount != 1062 { // 900 + 162
|
||||
t.Errorf("Expected Total 1062, got %f", item.TotalAmount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateInvoice(t *testing.T) {
|
||||
c := NewCalculator()
|
||||
product := Product{SKU: "TEST01", BasePrice: 100, GSTRate: Rate18}
|
||||
|
||||
item1 := c.CalculateLineItem(product, 1, CustomerRetail, false)
|
||||
item2 := c.CalculateLineItem(product, 1, CustomerRetail, false)
|
||||
|
||||
// Test with convenience fee (intra-state)
|
||||
invoice := c.CalculateInvoice([]LineItem{item1, item2}, 50, false)
|
||||
|
||||
// Convenience fee is taxed at 18%
|
||||
// SubTotal should include convenience fee: 100 + 100 + 50 = 250
|
||||
expectedSubTotal := 250.0
|
||||
if invoice.SubTotal != expectedSubTotal {
|
||||
t.Errorf("Expected SubTotal %f, got %f", expectedSubTotal, invoice.SubTotal)
|
||||
}
|
||||
|
||||
// Convenience fee tax: 50 * 0.18 = 9
|
||||
expectedFeeTax := 9.0
|
||||
if invoice.ConvenienceFeeTax != expectedFeeTax {
|
||||
t.Errorf("Expected ConvenienceFeeTax %f, got %f", expectedFeeTax, invoice.ConvenienceFeeTax)
|
||||
}
|
||||
|
||||
// Total CGST: 9 + 9 + 4.5 = 22.5 (from items + half of fee tax)
|
||||
expectedCGST := 22.5
|
||||
if invoice.TotalCGST != expectedCGST {
|
||||
t.Errorf("Expected TotalCGST %f, got %f", expectedCGST, invoice.TotalCGST)
|
||||
}
|
||||
|
||||
// GrandTotal: SubTotal + CGST + SGST = 250 + 22.5 + 22.5 = 295
|
||||
expectedTotal := 295.0
|
||||
if invoice.GrandTotal != expectedTotal {
|
||||
t.Errorf("Expected GrandTotal %f, got %f", expectedTotal, invoice.GrandTotal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateInvoiceInterState(t *testing.T) {
|
||||
c := NewCalculator()
|
||||
product := Product{SKU: "TEST01", BasePrice: 100, GSTRate: Rate18}
|
||||
|
||||
item := c.CalculateLineItem(product, 1, CustomerRetail, true)
|
||||
|
||||
// Test with convenience fee (inter-state)
|
||||
invoice := c.CalculateInvoice([]LineItem{item}, 50, true)
|
||||
|
||||
// SubTotal: 100 + 50 = 150
|
||||
expectedSubTotal := 150.0
|
||||
if invoice.SubTotal != expectedSubTotal {
|
||||
t.Errorf("Expected SubTotal %f, got %f", expectedSubTotal, invoice.SubTotal)
|
||||
}
|
||||
|
||||
// Convenience fee tax: 50 * 0.18 = 9
|
||||
expectedFeeTax := 9.0
|
||||
if invoice.ConvenienceFeeTax != expectedFeeTax {
|
||||
t.Errorf("Expected ConvenienceFeeTax %f, got %f", expectedFeeTax, invoice.ConvenienceFeeTax)
|
||||
}
|
||||
|
||||
// Total IGST: 18 + 9 = 27 (from item + fee tax)
|
||||
expectedIGST := 27.0
|
||||
if invoice.TotalIGST != expectedIGST {
|
||||
t.Errorf("Expected TotalIGST %f, got %f", expectedIGST, invoice.TotalIGST)
|
||||
}
|
||||
|
||||
// GrandTotal: SubTotal + IGST = 150 + 27 = 177
|
||||
expectedTotal := 177.0
|
||||
if invoice.GrandTotal != expectedTotal {
|
||||
t.Errorf("Expected GrandTotal %f, got %f", expectedTotal, invoice.GrandTotal)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user