The eight fresh lines at 1135 held 8, 12 and 18. Fresh unbranded fruit and chilled fish are nil-rated under Indian GST, so those were overcharging. Held back on the first pass and reported as REVIEW, because every one is a reduction of a live tax rate and that is a decision for whoever signs the returns rather than something a script should quietly do. Put to the owner and released explicitly. Two readings are assumed and are worth checking against what the counter actually sells. Maceral and Tuna are taken as fresh or chilled — frozen, branded or packaged fish is 5%. Hatsun curd was already 0 and stays there as plain curd; flavoured yoghurt would be 5%. The undo SQL for all eight is in the tool's output and restores the previous rates exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
181 lines
6.6 KiB
Go
181 lines
6.6 KiB
Go
// Set GST rates on the POS catalogue products.
|
|
//
|
|
// The four packaged lines at 1185 sit at taxpercent 0 and are being billed with
|
|
// no GST at all — a live compliance problem rather than a cosmetic one. Those
|
|
// are written.
|
|
//
|
|
// The produce at 1135 is NOT all zero, which is what this was first written
|
|
// believing. It holds 8, 12 and 18, and under Indian GST fresh unbranded fruit
|
|
// and chilled fish are nil-rated — so several look like overcharging. Every
|
|
// correction there is a *reduction* of a live rate, which is a decision for
|
|
// whoever signs the returns. Reported as REVIEW and left untouched.
|
|
//
|
|
// go run ./scratch/gstrates plan
|
|
// go run ./scratch/gstrates apply
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
|
|
"github.com/joho/godotenv"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
// Indian GST on food, as it applies to these lines.
|
|
//
|
|
// Fresh, unbranded and unpackaged produce is nil-rated, which is why the fruit
|
|
// stays at 0 rather than being "not set yet". Packaged branded snacks are 12%.
|
|
// Breakfast cereal is 18%.
|
|
//
|
|
// Fish is the one worth stating: fresh or chilled is nil-rated, and only
|
|
// frozen/branded/packaged attracts 5%. Left at 0 on the reading that a counter
|
|
// selling loose Mysore bananas is selling fresh fish, not frozen packs.
|
|
type rate struct {
|
|
productID int
|
|
name string
|
|
percent float64
|
|
why string
|
|
|
|
// apply gates the write. Only rows that are unambiguously *unset* are
|
|
// written; anything already carrying a rate is reported and left alone.
|
|
//
|
|
// The fresh produce at 1135 is the reason for this flag. Those rows are not
|
|
// blank — they hold 8, 12 and 18 — and under Indian GST fresh unbranded
|
|
// fruit and chilled fish are nil-rated, so several look like overcharging.
|
|
// But *lowering* a live tax rate is a compliance decision belonging to
|
|
// whoever signs the returns, not a bug to be quietly corrected by a script,
|
|
// and someone is actively working on pricing in this repo. Reported, not
|
|
// touched.
|
|
apply bool
|
|
}
|
|
|
|
var rates = []rate{
|
|
// 1135 — fresh produce, nil-rated under Indian GST.
|
|
//
|
|
// These were held back at first because every one is a *reduction* of a
|
|
// live rate, which is a compliance decision rather than a bug fix. Released
|
|
// on the owner's explicit instruction after that was put to them.
|
|
//
|
|
// Two readings are assumed and should be checked against what the counter
|
|
// actually sells: Maceral and Tuna are taken as fresh or chilled, which is
|
|
// nil-rated — frozen, branded or packaged fish is 5%. Hatsun curd is taken
|
|
// as plain curd, which is nil-rated — flavoured yoghurt is 5%.
|
|
{6988, "Mysore Banana", 0, "fresh fruit — nil-rated, currently 8%", true},
|
|
{6989, "Jammu Apple", 0, "fresh fruit — nil-rated, currently 18%", true},
|
|
{6990, "Small orange", 0, "fresh fruit — nil-rated, currently 18%", true},
|
|
{6991, "Red Guava", 0, "fresh fruit — nil-rated, currently 18%", true},
|
|
{6992, "Pomegrante", 0, "fresh fruit — nil-rated, currently 12%", true},
|
|
{6993, "Salem Mango", 0, "fresh fruit — nil-rated", true},
|
|
{6994, "Pineapple", 0, "fresh fruit — nil-rated", true},
|
|
{6995, "Strawberries", 0, "fresh fruit — nil-rated, currently 18%", true},
|
|
{6996, "Maceral", 0, "fresh fish nil-rated; 5% only if frozen/packaged", true},
|
|
{6997, "Tuna", 0, "fresh fish nil-rated; 5% only if frozen/packaged", true},
|
|
{6998, "Hatsun curd", 0, "curd nil-rated; flavoured yoghurt would be 5%", true},
|
|
{7014, "Apple", 0, "fresh fruit — nil-rated", true},
|
|
|
|
// 1185 — genuinely unset, and being billed with no GST at all today. This
|
|
// is the half that is unambiguous: every one is an increase from zero, so
|
|
// nothing is being under-collected on the strength of a script's opinion.
|
|
{7074, "Amla Dabur Oral Care Chewing Gum 10g", 18, "chewing gum, 18%", true},
|
|
{7075, "Cheetos Chips 100g", 12, "packaged extruded snack, 12%", true},
|
|
{7076, "Cheerios Breakfast Cereal 100g", 18, "packaged cereal, 18%", true},
|
|
{7077, "Hot Heads 30g", 12, "packaged snack, 12%", true},
|
|
}
|
|
|
|
func main() {
|
|
mode := "plan"
|
|
if len(os.Args) > 1 {
|
|
mode = os.Args[1]
|
|
}
|
|
|
|
_ = godotenv.Load()
|
|
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
|
os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"),
|
|
os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"))
|
|
|
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
write := mode == "apply"
|
|
|
|
fmt.Printf("%-6s %-38s %6s -> %6s %s\n", "id", "product", "now", "new", "why")
|
|
fmt.Println("-------------------------------------------------------------------------------------------")
|
|
|
|
undo := []string{}
|
|
changes := 0
|
|
review := 0
|
|
|
|
for _, r := range rates {
|
|
var current struct {
|
|
Taxpercent float64
|
|
Found bool
|
|
}
|
|
if err := db.Raw(`SELECT COALESCE(taxpercent, 0) AS taxpercent, true AS found
|
|
FROM products WHERE productid = ? LIMIT 1`,
|
|
r.productID).Scan(¤t).Error; err != nil {
|
|
log.Fatalf("reading %d: %v", r.productID, err)
|
|
}
|
|
if !current.Found {
|
|
fmt.Printf("%-6d %-38s NO products ROW - skipped\n", r.productID, r.name)
|
|
continue
|
|
}
|
|
|
|
if current.Taxpercent == r.percent {
|
|
fmt.Printf("%-6d %-38s %6.0f unchanged %s\n",
|
|
r.productID, r.name, current.Taxpercent, r.why)
|
|
continue
|
|
}
|
|
|
|
if !r.apply {
|
|
fmt.Printf("%-6d %-38s %6.0f REVIEW %-3.0f %s\n",
|
|
r.productID, r.name, current.Taxpercent, r.percent, r.why)
|
|
review++
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("%-6d %-38s %6.0f -> %6.0f %s\n",
|
|
r.productID, r.name, current.Taxpercent, r.percent, r.why)
|
|
|
|
undo = append(undo, fmt.Sprintf(
|
|
"UPDATE products SET taxpercent = %.0f WHERE productid = %d;",
|
|
current.Taxpercent, r.productID))
|
|
changes++
|
|
|
|
if write {
|
|
// updated is bumped so the catalogue delta carries the new rate to
|
|
// terminals holding a revision, rather than waiting for a full pull.
|
|
if err := db.Exec(`UPDATE products SET taxpercent = ?, updated = NOW()
|
|
WHERE productid = ?`, r.percent, r.productID).Error; err != nil {
|
|
log.Fatalf("writing %d: %v", r.productID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Println("-------------------------------------------------------------------------------------------")
|
|
if write {
|
|
fmt.Printf("APPLIED %d rate(s).\n", changes)
|
|
} else {
|
|
fmt.Printf("%d rate(s) would change. Nothing written — run `apply` to commit.\n", changes)
|
|
}
|
|
if review > 0 {
|
|
fmt.Printf("%d row(s) flagged REVIEW and deliberately not written — each is a\n"+
|
|
"reduction of a live tax rate and needs a decision, not a script.\n", review)
|
|
}
|
|
fmt.Println()
|
|
|
|
if len(undo) > 0 {
|
|
fmt.Println("-- undo:")
|
|
for _, u := range undo {
|
|
fmt.Println(u)
|
|
}
|
|
}
|
|
}
|