refactor: restructure in entirety

This commit is contained in:
Arkaprabha Chakraborty
2025-12-06 15:31:18 +05:30
parent 28733e22d3
commit 17a2bce744
43 changed files with 854 additions and 1342 deletions

166
internal/logic/auth.go Normal file
View File

@@ -0,0 +1,166 @@
package logic
import (
"billit/internal/models"
"crypto/rand"
"encoding/hex"
"errors"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
var (
ErrInvalidCredentials = errors.New("invalid email or password")
ErrInvalidToken = errors.New("invalid or expired token")
ErrUserNotFound = errors.New("user not found")
ErrUserExists = errors.New("user already exists")
)
// AuthConfig holds auth configuration
type AuthConfig struct {
JWTSecret []byte
TokenDuration time.Duration
}
// Claims represents JWT claims
type Claims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
// AuthService handles authentication logic
type AuthService struct {
config AuthConfig
users UserStore
}
// UserStore interface for user persistence (subset of database.Service)
type UserStore interface {
CreateUser(email, passwordHash string) (*models.User, error)
GetUserByEmail(email string) (*models.User, error)
GetUserByID(id string) (*models.User, error)
}
// NewAuthService creates a new auth service
func NewAuthService(users UserStore) *AuthService {
secret := os.Getenv("JWT_SECRET")
if secret == "" {
// Generate a random secret if not provided (not recommended for production)
b := make([]byte, 32)
rand.Read(b)
secret = hex.EncodeToString(b)
}
return &AuthService{
config: AuthConfig{
JWTSecret: []byte(secret),
TokenDuration: 24 * time.Hour,
},
users: users,
}
}
// HashPassword hashes a password using bcrypt with high cost
func HashPassword(password string) (string, error) {
// Use cost of 12 for good security/performance balance
hash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
if err != nil {
return "", err
}
return string(hash), nil
}
// CheckPassword verifies a password against a hash
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// Register creates a new user account
func (s *AuthService) Register(email, password string) (*models.User, error) {
// Check if user exists
existing, _ := s.users.GetUserByEmail(email)
if existing != nil {
return nil, ErrUserExists
}
// Validate password strength
if len(password) < 8 {
return nil, errors.New("password must be at least 8 characters")
}
// Hash password
hash, err := HashPassword(password)
if err != nil {
return nil, err
}
return s.users.CreateUser(email, hash)
}
// Login authenticates a user and returns a JWT token
func (s *AuthService) Login(email, password string) (string, error) {
user, err := s.users.GetUserByEmail(email)
if err != nil || user == nil {
return "", ErrInvalidCredentials
}
if !CheckPassword(password, user.Password) {
return "", ErrInvalidCredentials
}
return s.generateToken(user)
}
// generateToken creates a new JWT token for a user
func (s *AuthService) generateToken(user *models.User) (string, error) {
now := time.Now()
claims := &Claims{
UserID: user.ID,
Email: user.Email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(s.config.TokenDuration)),
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
Issuer: "billit",
Subject: user.ID,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.config.JWTSecret)
}
// ValidateToken validates a JWT token and returns the claims
func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, ErrInvalidToken
}
return s.config.JWTSecret, nil
})
if err != nil {
return nil, ErrInvalidToken
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, ErrInvalidToken
}
// GetUserFromToken retrieves the user from a valid token
func (s *AuthService) GetUserFromToken(tokenString string) (*models.User, error) {
claims, err := s.ValidateToken(tokenString)
if err != nil {
return nil, err
}
return s.users.GetUserByID(claims.UserID)
}

View File

@@ -0,0 +1,158 @@
package logic
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
}