test(explore): #1500 regression fixtures for budget allocation (CG-6)

Two permanent fixtures pinning the failure mode from issue #1500 — explore
spending its byte envelope on files that merely name-collide with the query.
BOTH FAIL TODAY, by design: they document the bug and become the pass gate
for CG-10 (scoring) + CG-12 (proportional allocation).

__tests__/fixtures/payroll-go/ — a synthetic Go service mirroring the
reporter's shape: generated FKIT CRUD beside a hand-written payroll use-case,
entered from an HTTP route. Half the generated tree carries ORDINARY names
detectable only by their `// Code generated ... DO NOT EDIT.` header (the
#1500 case, and end-to-end cover for CG-5); `payrollpb/*.pb.go` covers the
path-detectable channel. BuildPayslip, Upsert and Store each exist twice,
generated and hand-written. cycle.go sits above the whole-file window so it
clips; the generated files sit below it so they ship whole.

Asking "how does payroll cycle create and calculate payslips?" — naming none
of the answering symbols — the generated CRUD delivers 57.4% of the envelope
against the hand-written layer's 25.6%, all of the latter domain types.
cycle.go is allocated the single largest slice (30.6%) and delivers ZERO: the
hard ceiling drops its whole section. runPayrollCycleAll, the hand-written
BuildPayslip and the real Upsert never reach the agent.

The second fixture is this repo, "how does explore allocate its output budget
across files", where scripts/agent-eval/*.mjs take 71.8% against tools.ts's
18.5% despite scoring 4.6x lower. It reads the live index, so its assertions
are relative rather than fixed percentages.

- scripts/agent-eval/probe-allocation.mjs — per-file budget-share probe,
  driving the CG-4 diagnostic through a JSONL sidecar so it measures the
  shipping allocator. Fixture entries are hermetic (copy + re-index per run,
  verified byte-identical across runs); exits 1 while any assertion fails.
- scripts/agent-eval/allocation-fixtures.json — both fixtures declared, with
  the 2026-08-03 baselines.
- __tests__/explore-allocation-1500.test.ts — fixture-shape assertions green
  today; the allocation assertions held as `it.fails` so the suite stays green
  while the bug is open and goes RED the moment it is fixed.

Also documented and deliberately left unfixed: runPayrollCycleAll's
`s.store.Upsert` edge resolves to the GENERATED Store.Upsert, not the
hand-written one — same-name method resolution across two packages picks the
wrong receiver. It is upstream of the allocation bug, so it belongs with
CG-10's scoring work.

Refs #1500

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-03 23:30:17 -05:00
co-authored by Claude Opus 5
parent 16e17495f4
commit bd86ad2061
25 changed files with 2687 additions and 0 deletions
@@ -0,0 +1,84 @@
// Code generated by fkit v3.11.0. DO NOT EDIT.
//
// Source: schema/employee/contract.fkit
// Regenerate with: go run ./tools/fkitgen ./schema/employee
package employee
import (
"context"
"database/sql"
"time"
)
// ContractRow is the generated row type for table contract.
type ContractRow struct {
ID string
EmployeeID string
Kind string
Currency string
RateCents int64
PeriodRateCents int64
StartsOn time.Time
EndsOn time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
// ContractCreateRequest is the generated create payload for table contract.
type ContractCreateRequest struct {
EmployeeID string `json:"employeeId"`
Kind string `json:"kind"`
Currency string `json:"currency"`
RateCents int64 `json:"rateCents"`
PeriodRateCents int64 `json:"periodRateCents"`
StartsOn time.Time `json:"startsOn"`
}
// CreateContract inserts one contract row.
func CreateContract(ctx context.Context, db *sql.DB, req ContractCreateRequest) (ContractRow, error) {
const q = `INSERT INTO contract (employee_id, kind, currency, rate_cents, period_rate_cents, starts_on)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`
return scanContract(db.QueryRowContext(ctx, q, req.EmployeeID, req.Kind, req.Currency,
req.RateCents, req.PeriodRateCents, req.StartsOn))
}
// GetContract selects one contract row by primary key.
func GetContract(ctx context.Context, db *sql.DB, id string) (ContractRow, error) {
const q = `SELECT * FROM contract WHERE id = $1`
return scanContract(db.QueryRowContext(ctx, q, id))
}
// DeleteContract removes one contract row.
func DeleteContract(ctx context.Context, db *sql.DB, id string) error {
const q = `DELETE FROM contract WHERE id = $1`
_, err := db.ExecContext(ctx, q, id)
return err
}
// ListContractsForEmployee selects contract rows for one employee.
func ListContractsForEmployee(ctx context.Context, db *sql.DB, employeeID string) ([]ContractRow, error) {
const q = `SELECT * FROM contract WHERE employee_id = $1 ORDER BY starts_on DESC`
rows, err := db.QueryContext(ctx, q, employeeID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []ContractRow
for rows.Next() {
var r ContractRow
if err := rows.Scan(&r.ID, &r.EmployeeID, &r.Kind, &r.Currency, &r.RateCents,
&r.PeriodRateCents, &r.StartsOn, &r.EndsOn, &r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
func scanContract(row *sql.Row) (ContractRow, error) {
var r ContractRow
err := row.Scan(&r.ID, &r.EmployeeID, &r.Kind, &r.Currency, &r.RateCents,
&r.PeriodRateCents, &r.StartsOn, &r.EndsOn, &r.CreatedAt, &r.UpdatedAt)
return r, err
}
@@ -0,0 +1,88 @@
// Code generated by fkit v3.11.0. DO NOT EDIT.
//
// Source: schema/employee/employee.fkit
// Regenerate with: go run ./tools/fkitgen ./schema/employee
package employee
import (
"context"
"database/sql"
"time"
)
// EmployeeRow is the generated row type for table employee.
type EmployeeRow struct {
ID string
Email string
FullName string
Status string
HiredOn time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
// EmployeeCreateRequest is the generated create payload for table employee.
type EmployeeCreateRequest struct {
Email string `json:"email"`
FullName string `json:"fullName"`
Status string `json:"status"`
}
// EmployeeUpdateRequest is the generated update payload for table employee.
type EmployeeUpdateRequest struct {
FullName *string `json:"fullName,omitempty"`
Status *string `json:"status,omitempty"`
}
// CreateEmployee inserts one employee row.
func CreateEmployee(ctx context.Context, db *sql.DB, req EmployeeCreateRequest) (EmployeeRow, error) {
const q = `INSERT INTO employee (email, full_name, status) VALUES ($1, $2, $3) RETURNING *`
return scanEmployee(db.QueryRowContext(ctx, q, req.Email, req.FullName, req.Status))
}
// GetEmployee selects one employee row by primary key.
func GetEmployee(ctx context.Context, db *sql.DB, id string) (EmployeeRow, error) {
const q = `SELECT * FROM employee WHERE id = $1`
return scanEmployee(db.QueryRowContext(ctx, q, id))
}
// UpdateEmployee patches one employee row.
func UpdateEmployee(ctx context.Context, db *sql.DB, id string, req EmployeeUpdateRequest) (EmployeeRow, error) {
const q = `UPDATE employee SET full_name = COALESCE($2, full_name), status = COALESCE($3, status),
updated_at = now() WHERE id = $1 RETURNING *`
return scanEmployee(db.QueryRowContext(ctx, q, id, req.FullName, req.Status))
}
// DeleteEmployee removes one employee row.
func DeleteEmployee(ctx context.Context, db *sql.DB, id string) error {
const q = `DELETE FROM employee WHERE id = $1`
_, err := db.ExecContext(ctx, q, id)
return err
}
// ListEmployeesByStatus selects employee rows in one status.
func ListEmployeesByStatus(ctx context.Context, db *sql.DB, status string) ([]EmployeeRow, error) {
const q = `SELECT * FROM employee WHERE status = $1 ORDER BY full_name`
rows, err := db.QueryContext(ctx, q, status)
if err != nil {
return nil, err
}
defer rows.Close()
var out []EmployeeRow
for rows.Next() {
var r EmployeeRow
if err := rows.Scan(&r.ID, &r.Email, &r.FullName, &r.Status, &r.HiredOn,
&r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
func scanEmployee(row *sql.Row) (EmployeeRow, error) {
var r EmployeeRow
err := row.Scan(&r.ID, &r.Email, &r.FullName, &r.Status, &r.HiredOn, &r.CreatedAt, &r.UpdatedAt)
return r, err
}
@@ -0,0 +1,60 @@
// Code generated by fkit v3.11.0. DO NOT EDIT.
//
// Source: schema/payroll/aggregates.fkit
// Regenerate with: go run ./tools/fkitgen ./schema/payroll
package payroll
import (
"context"
"database/sql"
)
// PayrollCycleTotals is the generated aggregate row for a payroll cycle.
type PayrollCycleTotals struct {
CycleID string
Payslips int64
GrossCents int64
DeductionCents int64
NetCents int64
}
// CalculatePayrollCycleTotals runs the generated SUM aggregate over the
// payslip rows of one cycle. It totals what is already stored; it does not
// calculate any payslip.
func CalculatePayrollCycleTotals(ctx context.Context, db *sql.DB, cycleID string) (PayrollCycleTotals, error) {
const q = `SELECT count(*), COALESCE(sum(gross_cents), 0), COALESCE(sum(deduction_cents), 0),
COALESCE(sum(net_cents), 0)
FROM payslip WHERE cycle_id = $1`
var t PayrollCycleTotals
t.CycleID = cycleID
err := db.QueryRowContext(ctx, q, cycleID).Scan(&t.Payslips, &t.GrossCents, &t.DeductionCents, &t.NetCents)
return t, err
}
// CalculatePayslipNet recomputes net from the stored gross and deduction
// columns of one row. Pure column arithmetic — no pay rules.
func CalculatePayslipNet(row PayslipRow) int64 {
return row.GrossCents - row.DeductionCents
}
// CalculatePayrollCycleAverage averages the stored net over a cycle.
func CalculatePayrollCycleAverage(ctx context.Context, db *sql.DB, cycleID string) (int64, error) {
totals, err := CalculatePayrollCycleTotals(ctx, db, cycleID)
if err != nil {
return 0, err
}
if totals.Payslips == 0 {
return 0, nil
}
return totals.NetCents / totals.Payslips, nil
}
// CalculateEmployeeYearToDate sums an employee's stored payslips for a year.
func CalculateEmployeeYearToDate(ctx context.Context, db *sql.DB, employeeID string, year int) (int64, error) {
const q = `SELECT COALESCE(sum(net_cents), 0) FROM payslip
WHERE employee_id = $1 AND extract(year from period_from) = $2`
var n int64
err := db.QueryRowContext(ctx, q, employeeID, year).Scan(&n)
return n, err
}
@@ -0,0 +1,80 @@
// Code generated by fkit v3.11.0. DO NOT EDIT.
//
// Source: schema/payroll
// Regenerate with: go run ./tools/fkitgen ./schema/payroll
package payroll
import "time"
// PayslipDTO is the generated wire representation of a payslip row.
type PayslipDTO struct {
ID string `json:"id"`
CycleID string `json:"cycleId"`
EmployeeID string `json:"employeeId"`
Currency string `json:"currency"`
PeriodFrom time.Time `json:"periodFrom"`
PeriodTo time.Time `json:"periodTo"`
GrossCents int64 `json:"grossCents"`
DeductionCents int64 `json:"deductionCents"`
NetCents int64 `json:"netCents"`
}
// PayrollCycleDTO is the generated wire representation of a payroll_cycle row.
type PayrollCycleDTO struct {
ID string `json:"id"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Status string `json:"status"`
}
// PayslipListDTO is the generated list envelope for payslip rows.
type PayslipListDTO struct {
Items []PayslipDTO `json:"items"`
NextCursor string `json:"nextCursor,omitempty"`
Total int64 `json:"total"`
}
// PayslipToDTO converts a payslip row to its wire form.
func PayslipToDTO(r PayslipRow) PayslipDTO {
return PayslipDTO{
ID: r.ID,
CycleID: r.CycleID,
EmployeeID: r.EmployeeID,
Currency: r.Currency,
PeriodFrom: r.PeriodFrom,
PeriodTo: r.PeriodTo,
GrossCents: r.GrossCents,
DeductionCents: r.DeductionCents,
NetCents: r.NetCents,
}
}
// PayslipFromDTO converts a wire payslip back to a row.
func PayslipFromDTO(d PayslipDTO) PayslipRow {
return PayslipRow{
ID: d.ID,
CycleID: d.CycleID,
EmployeeID: d.EmployeeID,
Currency: d.Currency,
PeriodFrom: d.PeriodFrom,
PeriodTo: d.PeriodTo,
GrossCents: d.GrossCents,
DeductionCents: d.DeductionCents,
NetCents: d.NetCents,
}
}
// PayrollCycleToDTO converts a payroll_cycle row to its wire form.
func PayrollCycleToDTO(r PayrollCycleRow) PayrollCycleDTO {
return PayrollCycleDTO{ID: r.ID, Start: r.Start, End: r.End, Status: r.Status}
}
// PayslipsToListDTO wraps payslip rows in the generated list envelope.
func PayslipsToListDTO(rows []PayslipRow, total int64) PayslipListDTO {
items := make([]PayslipDTO, 0, len(rows))
for _, r := range rows {
items = append(items, PayslipToDTO(r))
}
return PayslipListDTO{Items: items, Total: total}
}
@@ -0,0 +1,116 @@
// Code generated by fkit v3.11.0. DO NOT EDIT.
//
// Source: schema/payroll/payroll_cycle.fkit
// Regenerate with: go run ./tools/fkitgen ./schema/payroll
package payroll
import (
"context"
"database/sql"
"time"
)
// PayrollCycleRow is the generated row type for table payroll_cycle.
type PayrollCycleRow struct {
ID string
Start time.Time
End time.Time
Status string
ClosedAt time.Time
ReopenReason string
CreatedAt time.Time
UpdatedAt time.Time
}
// PayrollCycleCreateRequest is the generated create payload for payroll_cycle.
type PayrollCycleCreateRequest struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Status string `json:"status"`
}
// PayrollCycleUpdateRequest is the generated update payload for payroll_cycle.
type PayrollCycleUpdateRequest struct {
Status *string `json:"status,omitempty"`
ReopenReason *string `json:"reopenReason,omitempty"`
}
// CreatePayrollCycle inserts one payroll_cycle row.
func CreatePayrollCycle(ctx context.Context, db *sql.DB, req PayrollCycleCreateRequest) (PayrollCycleRow, error) {
const q = `INSERT INTO payroll_cycle (start_on, end_on, status) VALUES ($1, $2, $3) RETURNING *`
return scanPayrollCycle(db.QueryRowContext(ctx, q, req.Start, req.End, req.Status))
}
// GetPayrollCycle selects one payroll_cycle row by primary key.
func GetPayrollCycle(ctx context.Context, db *sql.DB, id string) (PayrollCycleRow, error) {
const q = `SELECT * FROM payroll_cycle WHERE id = $1`
return scanPayrollCycle(db.QueryRowContext(ctx, q, id))
}
// UpdatePayrollCycle patches one payroll_cycle row.
func UpdatePayrollCycle(ctx context.Context, db *sql.DB, id string, req PayrollCycleUpdateRequest) (PayrollCycleRow, error) {
const q = `UPDATE payroll_cycle SET status = COALESCE($2, status),
reopen_reason = COALESCE($3, reopen_reason), updated_at = now()
WHERE id = $1 RETURNING *`
return scanPayrollCycle(db.QueryRowContext(ctx, q, id, req.Status, req.ReopenReason))
}
// DeletePayrollCycle removes one payroll_cycle row.
func DeletePayrollCycle(ctx context.Context, db *sql.DB, id string) error {
const q = `DELETE FROM payroll_cycle WHERE id = $1`
_, err := db.ExecContext(ctx, q, id)
return err
}
// ListPayrollCycles selects every payroll_cycle row.
func ListPayrollCycles(ctx context.Context, db *sql.DB) ([]PayrollCycleRow, error) {
const q = `SELECT * FROM payroll_cycle ORDER BY start_on DESC`
rows, err := db.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
var out []PayrollCycleRow
for rows.Next() {
var r PayrollCycleRow
if err := rows.Scan(&r.ID, &r.Start, &r.End, &r.Status, &r.ClosedAt, &r.ReopenReason,
&r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// ListPayrollCyclesByStatus selects payroll_cycle rows in one status.
func ListPayrollCyclesByStatus(ctx context.Context, db *sql.DB, status string) ([]PayrollCycleRow, error) {
const q = `SELECT * FROM payroll_cycle WHERE status = $1 ORDER BY start_on DESC`
rows, err := db.QueryContext(ctx, q, status)
if err != nil {
return nil, err
}
defer rows.Close()
var out []PayrollCycleRow
for rows.Next() {
var r PayrollCycleRow
if err := rows.Scan(&r.ID, &r.Start, &r.End, &r.Status, &r.ClosedAt, &r.ReopenReason,
&r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// BuildPayrollCycle maps a create request onto a row.
func BuildPayrollCycle(req PayrollCycleCreateRequest) PayrollCycleRow {
return PayrollCycleRow{Start: req.Start, End: req.End, Status: req.Status}
}
func scanPayrollCycle(row *sql.Row) (PayrollCycleRow, error) {
var r PayrollCycleRow
err := row.Scan(&r.ID, &r.Start, &r.End, &r.Status, &r.ClosedAt, &r.ReopenReason,
&r.CreatedAt, &r.UpdatedAt)
return r, err
}
@@ -0,0 +1,129 @@
// Code generated by fkit v3.11.0. DO NOT EDIT.
//
// Source: schema/payroll/payslip.fkit
// Regenerate with: go run ./tools/fkitgen ./schema/payroll
package payroll
import (
"context"
"database/sql"
"time"
)
// PayslipRow is the generated row type for table payslip.
type PayslipRow struct {
ID string
CycleID string
EmployeeID string
Currency string
PeriodFrom time.Time
PeriodTo time.Time
GrossCents int64
DeductionCents int64
NetCents int64
Underwater bool
RunAt time.Time
RunReason string
CreatedAt time.Time
UpdatedAt time.Time
}
// PayslipCreateRequest is the generated create payload for table payslip.
type PayslipCreateRequest struct {
CycleID string `json:"cycleId"`
EmployeeID string `json:"employeeId"`
Currency string `json:"currency"`
GrossCents int64 `json:"grossCents"`
DeductionCents int64 `json:"deductionCents"`
NetCents int64 `json:"netCents"`
}
// PayslipUpdateRequest is the generated update payload for table payslip.
type PayslipUpdateRequest struct {
GrossCents *int64 `json:"grossCents,omitempty"`
DeductionCents *int64 `json:"deductionCents,omitempty"`
NetCents *int64 `json:"netCents,omitempty"`
RunReason *string `json:"runReason,omitempty"`
}
// CreatePayslip inserts one payslip row.
func CreatePayslip(ctx context.Context, db *sql.DB, req PayslipCreateRequest) (PayslipRow, error) {
const q = `INSERT INTO payslip (cycle_id, employee_id, currency, gross_cents, deduction_cents, net_cents)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`
row := db.QueryRowContext(ctx, q, req.CycleID, req.EmployeeID, req.Currency, req.GrossCents, req.DeductionCents, req.NetCents)
return scanPayslip(row)
}
// GetPayslip selects one payslip row by primary key.
func GetPayslip(ctx context.Context, db *sql.DB, id string) (PayslipRow, error) {
const q = `SELECT * FROM payslip WHERE id = $1`
return scanPayslip(db.QueryRowContext(ctx, q, id))
}
// UpdatePayslip patches one payslip row.
func UpdatePayslip(ctx context.Context, db *sql.DB, id string, req PayslipUpdateRequest) (PayslipRow, error) {
const q = `UPDATE payslip SET gross_cents = COALESCE($2, gross_cents),
deduction_cents = COALESCE($3, deduction_cents),
net_cents = COALESCE($4, net_cents),
run_reason = COALESCE($5, run_reason),
updated_at = now() WHERE id = $1 RETURNING *`
return scanPayslip(db.QueryRowContext(ctx, q, id, req.GrossCents, req.DeductionCents, req.NetCents, req.RunReason))
}
// DeletePayslip removes one payslip row.
func DeletePayslip(ctx context.Context, db *sql.DB, id string) error {
const q = `DELETE FROM payslip WHERE id = $1`
_, err := db.ExecContext(ctx, q, id)
return err
}
// ListPayslipsByCycle selects every payslip row for a cycle.
func ListPayslipsByCycle(ctx context.Context, db *sql.DB, cycleID string) ([]PayslipRow, error) {
const q = `SELECT * FROM payslip WHERE cycle_id = $1 ORDER BY employee_id`
rows, err := db.QueryContext(ctx, q, cycleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []PayslipRow
for rows.Next() {
var r PayslipRow
if err := rows.Scan(&r.ID, &r.CycleID, &r.EmployeeID, &r.Currency, &r.PeriodFrom, &r.PeriodTo,
&r.GrossCents, &r.DeductionCents, &r.NetCents, &r.Underwater, &r.RunAt, &r.RunReason,
&r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// CountPayslipsByCycle counts payslip rows for a cycle.
func CountPayslipsByCycle(ctx context.Context, db *sql.DB, cycleID string) (int64, error) {
const q = `SELECT count(*) FROM payslip WHERE cycle_id = $1`
var n int64
err := db.QueryRowContext(ctx, q, cycleID).Scan(&n)
return n, err
}
// BuildPayslip maps a create request onto a row. Field copy only — the
// generator has no knowledge of pay rules.
func BuildPayslip(req PayslipCreateRequest) PayslipRow {
return PayslipRow{
CycleID: req.CycleID,
EmployeeID: req.EmployeeID,
Currency: req.Currency,
GrossCents: req.GrossCents,
DeductionCents: req.DeductionCents,
NetCents: req.NetCents,
}
}
func scanPayslip(row *sql.Row) (PayslipRow, error) {
var r PayslipRow
err := row.Scan(&r.ID, &r.CycleID, &r.EmployeeID, &r.Currency, &r.PeriodFrom, &r.PeriodTo,
&r.GrossCents, &r.DeductionCents, &r.NetCents, &r.Underwater, &r.RunAt, &r.RunReason,
&r.CreatedAt, &r.UpdatedAt)
return r, err
}
@@ -0,0 +1,95 @@
// Code generated by fkit v3.11.0. DO NOT EDIT.
//
// Source: schema/payroll
// Regenerate with: go run ./tools/fkitgen ./schema/payroll
package payroll
import (
"context"
"database/sql"
)
// Store is the generated repository over every payroll table.
type Store struct {
db *sql.DB
}
// NewStore returns a generated store bound to db.
func NewStore(db *sql.DB) *Store { return &Store{db: db} }
// Upsert writes one payslip row, keyed by (cycle_id, employee_id).
func (s *Store) Upsert(ctx context.Context, row PayslipRow) (PayslipRow, error) {
const q = `INSERT INTO payslip (cycle_id, employee_id, currency, gross_cents, deduction_cents, net_cents)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (cycle_id, employee_id) DO UPDATE SET
gross_cents = EXCLUDED.gross_cents,
deduction_cents = EXCLUDED.deduction_cents,
net_cents = EXCLUDED.net_cents,
updated_at = now()
RETURNING *`
return scanPayslip(s.db.QueryRowContext(ctx, q, row.CycleID, row.EmployeeID, row.Currency,
row.GrossCents, row.DeductionCents, row.NetCents))
}
// UpsertPayrollCycle writes one payroll_cycle row, keyed by id.
func (s *Store) UpsertPayrollCycle(ctx context.Context, row PayrollCycleRow) (PayrollCycleRow, error) {
const q = `INSERT INTO payroll_cycle (id, start_on, end_on, status)
VALUES ($1, $2, $3, $4)
ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, updated_at = now()
RETURNING *`
return scanPayrollCycle(s.db.QueryRowContext(ctx, q, row.ID, row.Start, row.End, row.Status))
}
// CreatePayslip inserts one payslip row through the store.
func (s *Store) CreatePayslip(ctx context.Context, req PayslipCreateRequest) (PayslipRow, error) {
return CreatePayslip(ctx, s.db, req)
}
// GetPayslip reads one payslip row through the store.
func (s *Store) GetPayslip(ctx context.Context, id string) (PayslipRow, error) {
return GetPayslip(ctx, s.db, id)
}
// UpdatePayslip patches one payslip row through the store.
func (s *Store) UpdatePayslip(ctx context.Context, id string, req PayslipUpdateRequest) (PayslipRow, error) {
return UpdatePayslip(ctx, s.db, id, req)
}
// DeletePayslip removes one payslip row through the store.
func (s *Store) DeletePayslip(ctx context.Context, id string) error {
return DeletePayslip(ctx, s.db, id)
}
// ListPayslipsByCycle lists payslip rows for a cycle through the store.
func (s *Store) ListPayslipsByCycle(ctx context.Context, cycleID string) ([]PayslipRow, error) {
return ListPayslipsByCycle(ctx, s.db, cycleID)
}
// CreatePayrollCycle inserts one payroll_cycle row through the store.
func (s *Store) CreatePayrollCycle(ctx context.Context, req PayrollCycleCreateRequest) (PayrollCycleRow, error) {
return CreatePayrollCycle(ctx, s.db, req)
}
// GetPayrollCycle reads one payroll_cycle row through the store.
func (s *Store) GetPayrollCycle(ctx context.Context, id string) (PayrollCycleRow, error) {
return GetPayrollCycle(ctx, s.db, id)
}
// ListPayrollCycles lists payroll_cycle rows through the store.
func (s *Store) ListPayrollCycles(ctx context.Context) ([]PayrollCycleRow, error) {
return ListPayrollCycles(ctx, s.db)
}
// Tx runs fn inside a transaction.
func (s *Store) Tx(ctx context.Context, fn func(*Store) error) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
if err := fn(s); err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
@@ -0,0 +1,76 @@
// Code generated by fkit v3.11.0. DO NOT EDIT.
//
// Source: schema/timesheet/timesheet.fkit
// Regenerate with: go run ./tools/fkitgen ./schema/timesheet
package timesheet
import (
"context"
"database/sql"
"time"
)
// TimesheetRow is the generated row type for table timesheet.
type TimesheetRow struct {
ID string
CycleID string
EmployeeID string
Units int
Approved bool
ApprovedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
// TimesheetCreateRequest is the generated create payload for table timesheet.
type TimesheetCreateRequest struct {
CycleID string `json:"cycleId"`
EmployeeID string `json:"employeeId"`
Units int `json:"units"`
}
// CreateTimesheet inserts one timesheet row.
func CreateTimesheet(ctx context.Context, db *sql.DB, req TimesheetCreateRequest) (TimesheetRow, error) {
const q = `INSERT INTO timesheet (cycle_id, employee_id, units) VALUES ($1, $2, $3) RETURNING *`
return scanTimesheet(db.QueryRowContext(ctx, q, req.CycleID, req.EmployeeID, req.Units))
}
// GetTimesheet selects one timesheet row by primary key.
func GetTimesheet(ctx context.Context, db *sql.DB, id string) (TimesheetRow, error) {
const q = `SELECT * FROM timesheet WHERE id = $1`
return scanTimesheet(db.QueryRowContext(ctx, q, id))
}
// ApproveTimesheet flips the approved column on one timesheet row.
func ApproveTimesheet(ctx context.Context, db *sql.DB, id string) (TimesheetRow, error) {
const q = `UPDATE timesheet SET approved = true, approved_at = now() WHERE id = $1 RETURNING *`
return scanTimesheet(db.QueryRowContext(ctx, q, id))
}
// ListTimesheetsByCycle selects timesheet rows for one cycle.
func ListTimesheetsByCycle(ctx context.Context, db *sql.DB, cycleID string) ([]TimesheetRow, error) {
const q = `SELECT * FROM timesheet WHERE cycle_id = $1 ORDER BY employee_id`
rows, err := db.QueryContext(ctx, q, cycleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []TimesheetRow
for rows.Next() {
var r TimesheetRow
if err := rows.Scan(&r.ID, &r.CycleID, &r.EmployeeID, &r.Units, &r.Approved,
&r.ApprovedAt, &r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
func scanTimesheet(row *sql.Row) (TimesheetRow, error) {
var r TimesheetRow
err := row.Scan(&r.ID, &r.CycleID, &r.EmployeeID, &r.Units, &r.Approved,
&r.ApprovedAt, &r.CreatedAt, &r.UpdatedAt)
return r, err
}