Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .claude/skills/report-an-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ Then help them write the *private* report using the same evidence-gathering belo
here — do not draft a public issue.

**The one exception people trip over:** the demo and staging deployments run with
`AUTH_MODE=dev`, which intentionally disables login and treats everyone as the seeded
`demo@gable.com` admin. On a deployment that is *deliberately* a demo or staging sandbox
`AUTH_MODE=dev`, which intentionally disables login and treats everyone as an admin. On a deployment that is *deliberately* a demo or staging sandbox
that is expected and safe (the data is fake). It is only a security issue if you find it on
a host that is meant to be real. See `SECURITY.md` § "`AUTH_MODE=dev` must never reach production".

Expand Down
2 changes: 1 addition & 1 deletion core/internal/app/seed/demodata.go
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ func importCustomers(ctx context.Context, db *sql.DB, yards map[string]uuid.UUID

// importStaff reads demodata/staff.jsonl and upserts each row into staff
// and user_locations (the staff-home / yard grant). The legacy seed's three
// AI_LM demo rows (dispatcher@gable.com, yard@gable.com, admin@gable.com)
// AI_LM demo rows (dispatcher@gablebuildingsupply.example, yard@gablebuildingsupply.example, admin@gablebuildingsupply.example)
// and the nine multi yard personas live in the same table and are preserved.
func importStaff(ctx context.Context, db *sql.DB, yards map[string]uuid.UUID) error {
scanner := bufio.NewScanner(bytes.NewReader(staffData))
Expand Down
126 changes: 63 additions & 63 deletions core/internal/app/seed/demodata/catalog.jsonl

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions core/internal/app/seed/demodata/customers.jsonl

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions core/internal/app/seed/demodata/staff.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
{"email":"buyer.hardware.paint@brand.example","full_name":"Joel Aldridge","staff_no":"STF-015","role":"buyer_hardware_paint","home_yard":"WK","active":true}
{"email":"buyer.millwork.doors@brand.example","full_name":"Rhea Jamison","staff_no":"STF-016","role":"buyer_millwork_doors","home_yard":"WK","active":true}
{"email":"buyer.seasonal.lawn@brand.example","full_name":"Theo Caldwell","staff_no":"STF-017","role":"buyer_seasonal_lawn","home_yard":"LK","active":true}
{"email":"home.center.associate.kel.1@brand.example","full_name":"Una Zellweger","staff_no":"STF-018","role":"home_center_associate","home_yard":"KEL-MAIN","active":true}
{"email":"home.center.associate.kel.1@brand.example","full_name":"Una Wexcombe","staff_no":"STF-018","role":"home_center_associate","home_yard":"KEL-MAIN","active":true}
{"email":"home.center.associate.kel.2@brand.example","full_name":"Yara Yardley","staff_no":"STF-019","role":"home_center_associate","home_yard":"KEL-MAIN","active":true}
{"email":"home.center.associate.kel.3@brand.example","full_name":"Quinn Gardiner","staff_no":"STF-020","role":"home_center_associate","home_yard":"KEL-MAIN","active":true}
{"email":"home.center.associate.wk.1@brand.example","full_name":"Vince Nordstrom","staff_no":"STF-021","role":"home_center_associate","home_yard":"WK","active":true}
{"email":"home.center.associate.wk.1@brand.example","full_name":"Vince Ashgrove","staff_no":"STF-021","role":"home_center_associate","home_yard":"WK","active":true}
{"email":"home.center.associate.wk.2@brand.example","full_name":"Olga Fairweather","staff_no":"STF-022","role":"home_center_associate","home_yard":"WK","active":true}
{"email":"home.center.associate.lk.1@brand.example","full_name":"Kira Bergstrom","staff_no":"STF-023","role":"home_center_associate","home_yard":"LK","active":true}
{"email":"cashier.kel.1@brand.example","full_name":"Tara Kellogg","staff_no":"STF-024","role":"cashier","home_yard":"KEL-MAIN","active":true}
{"email":"cashier.kel.1@brand.example","full_name":"Tara Penhallow","staff_no":"STF-024","role":"cashier","home_yard":"KEL-MAIN","active":true}
{"email":"cashier.kel.2@brand.example","full_name":"Mira Vanderpol","staff_no":"STF-025","role":"cashier","home_yard":"KEL-MAIN","active":true}
{"email":"cashier.wk.1@brand.example","full_name":"Finn Underhill","staff_no":"STF-026","role":"cashier","home_yard":"WK","active":true}
{"email":"cashier.lk.1@brand.example","full_name":"Avril MacNaught","staff_no":"STF-027","role":"cashier","home_yard":"LK","active":true}
Expand Down
4 changes: 2 additions & 2 deletions core/internal/app/seed/demodata/vendor_keys.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@
"notes": "Invented connector brand. Joist hangers, hurricane ties, hold downs."
},
{
"key": "CEMTRIM_OK",
"name": "Okanagan Cemtrim Ltd",
"key": "FIBRETRIM_OK",
"name": "Okanagan Fibre Trim Ltd",
"category": "Fibre cement siding and trim",
"payment_terms": "Net 30",
"average_lead_time_days": 10,
Expand Down
62 changes: 61 additions & 1 deletion core/internal/app/seed/demodata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,50 @@ func TestDemoImportRequested(t *testing.T) {
}
}

// bannedSeedWords are real brands and a real domain the seeds and the import
// never carry: a product is named for what it is, and a demo address ends in
// a reserved ".example" domain.
var bannedSeedWords = []string{
"simpson", "strong-tie", "hardie", "cemtrim", "channellock", "romex",
"dryline", "firecode", "brownwood", "turtle", "gable.com",
"nordstrom", "kellogg", "zellweger",
"simp-", "ice & water shield", "speed square",
}

// TestSeedDataHoldsNoBrandWord reads the embedded import and fails on any
// banned word, so the check needs no database.
func TestSeedDataHoldsNoBrandWord(t *testing.T) {
for name, data := range map[string][]byte{
"catalog": catalogData,
"customers": customersData,
"staff": staffData,
"vendors": vendorsData,
"vendorkeys": vendorKeysData,
} {
lower := strings.ToLower(string(data))
for _, word := range bannedSeedWords {
if strings.Contains(lower, word) {
t.Errorf("%s import holds %q", name, word)
}
}
}
}

func TestDemoImport(t *testing.T) {
if testing.Short() {
t.Skip("seeds a scratch database of its own; skipped under -short")
}
db, dbURL := demoImportDB(t)
runCommand(t, dbURL, "./cmd/migrate")
// Migrations 080 and 118 together: a migrated database, before any seed
// run, holds no row on the real domain.
var onRealDomain int
if err := db.QueryRow(`SELECT COUNT(*) FROM staff WHERE email ILIKE '%@gable.com'`).Scan(&onRealDomain); err != nil {
t.Fatalf("count staff on the real domain: %v", err)
}
if onRealDomain != 0 {
t.Errorf("a migrated database holds %d staff rows on @gable.com", onRealDomain)
}
// The tenant takes a currency other than the documents' column default
// (USD), so a history document that fell back to the default would show.
res, err := db.Exec(`UPDATE system_settings SET value = 'CAD' WHERE key IN ('currency.default', 'currency.enabled')`)
Expand Down Expand Up @@ -161,7 +199,6 @@ func TestDemoImport(t *testing.T) {
AND sku NOT LIKE 'SCR-%'
AND sku NOT LIKE 'HANGER-%'
AND sku NOT LIKE 'TIE-%'
AND sku NOT LIKE 'SIMP-%'
AND sku NOT LIKE 'RF-%'
AND sku NOT LIKE 'INS-%'
AND sku NOT LIKE 'DR-%'
Expand Down Expand Up @@ -213,6 +250,29 @@ func TestDemoImport(t *testing.T) {
}
})

t.Run("no row carries a brand word or the real domain", func(t *testing.T) {
for _, c := range []struct{ table, column string }{
{"products", "sku"},
{"products", "description"},
{"products", "vendor"},
{"products", "category"},
{"vendors", "name"},
{"staff", "email"},
{"staff", "full_name"},
} {
for _, word := range bannedSeedWords {
var n int
q := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE %s ILIKE $1`, c.table, c.column)
if err := db.QueryRow(q, "%"+word+"%").Scan(&n); err != nil {
t.Fatalf("%s.%s: %v", c.table, c.column, err)
}
if n > 0 {
t.Errorf("%s.%s: %d rows hold %q", c.table, c.column, n, word)
}
}
}
})

t.Run("revenue trends across many days at each yard", func(t *testing.T) {
var days int
if err := db.QueryRow(`
Expand Down
2 changes: 1 addition & 1 deletion core/internal/app/seed/dispatch_day.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ var productGeometry = []geometry{
noGeometry("HANGER-28"),
noGeometry("HANGER-210"),
noGeometry("TIE-H1"),
noGeometry("SIMP-LUS28"),
noGeometry("HANGER-28FM"),

// --- Insulation: compressed bags ---
dims("INS-R13-15", 48, 16, 20, true),
Expand Down
2 changes: 1 addition & 1 deletion core/internal/app/seed/product_pictures.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var baseProductFamilies = map[string]string{
"NAIL-16D-50": "common_nail_box", "NAIL-10D-50": "common_nail_box", "RF-NAIL-125": "galvanized_nail_box",
"SCR-DECK-3-5": "deck_screw_box",
"HANGER-26": "joist_hanger", "HANGER-28": "joist_hanger", "HANGER-210": "joist_hanger",
"SIMP-LUS28": "joist_hanger", "TIE-H1": "framing_connector",
"HANGER-28FM": "joist_hanger", "TIE-H1": "framing_connector",
// roofing
"RF-ARCH-30": "asphalt_shingle_bundle", "RF-SH-BLK": "asphalt_shingle_bundle",
"RF-SH-WW": "asphalt_shingle_bundle", "RF-START": "asphalt_shingle_bundle",
Expand Down
46 changes: 46 additions & 0 deletions core/internal/app/seed/product_pictures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"encoding/json"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)

Expand Down Expand Up @@ -39,3 +41,47 @@ func TestEveryPicturePathHasAnIllustration(t *testing.T) {
}
}
}

// RULE: the base seed's picture map keys only SKUs the base seed writes, and
// no key holds a brand word, so a renamed product keeps its picture.
func TestBasePictureKeysAreSeededSKUs(t *testing.T) {
src, err := os.ReadFile("seed.go")
if err != nil {
t.Fatal(err)
}
for sku := range baseProductFamilies {
if !strings.Contains(string(src), `"`+sku+`"`) {
t.Errorf("picture key %q is not a SKU the base seed writes", sku)
}
for _, word := range bannedSeedWords {
if strings.Contains(strings.ToLower(sku), word) {
t.Errorf("picture key %q holds %q", sku, word)
}
}
}
}

// RULE: no illustration's title or text names a maker's mark.
func TestIllustrationsNameNoBrand(t *testing.T) {
dir := filepath.Join("..", "..", "..", "..", "web", "apps", "desk", "public", "catalog-illustrations")
files, err := filepath.Glob(filepath.Join(dir, "*.svg"))
if err != nil || len(files) == 0 {
t.Fatalf("no illustrations found: %v", err)
}
decora := regexp.MustCompile(`\bdecora\b`)
for _, f := range files {
raw, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
lower := strings.ToLower(string(raw)) + " " + strings.ToLower(filepath.Base(f))
for _, word := range bannedSeedWords {
if strings.Contains(lower, word) {
t.Errorf("%s holds %q", filepath.Base(f), word)
}
}
if decora.MatchString(lower) {
t.Errorf("%s holds the mark Decora", filepath.Base(f))
}
}
}
24 changes: 12 additions & 12 deletions core/internal/app/seed/seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,27 +470,27 @@ func Run() {
{"3/4 OSB T&G 4x8", "OSB-34-TG", "PCS", "Gable Building Supply", "Sheet Goods", 22.00, 34.00, 65.0, 100, 250},
{"7/16 OSB 4x8", "OSB-12", "PCS", "Gable Building Supply", "Sheet Goods", 12.00, 19.50, 44.0, 120, 300},
{"1/2 Drywall Regular 4x8", "DW-12-REG", "PCS", "Gable Building Supply", "Sheet Goods", 11.00, 16.00, 57.0, 100, 250},
{"5/8 Drywall Firecode 4x8", "DW-58-FC", "PCS", "Gable Building Supply", "Sheet Goods", 14.00, 21.00, 70.0, 60, 150},
{"5/8 Drywall Type X 4x8", "DW-58-FC", "PCS", "Gable Building Supply", "Sheet Goods", 14.00, 21.00, 70.0, 60, 150},
{"3-1/4\" Common Nails (25kg)", "NAIL-16D-50", "BOX", "Mountain Fastener Supply", "Hardware", 45.00, 65.00, 50.0, 20, 50},
{"3\" Common Nails (25kg)", "NAIL-10D-50", "BOX", "Mountain Fastener Supply", "Hardware", 45.00, 65.00, 50.0, 20, 50},
{"3\" Deck Screws (2.5kg)", "SCR-DECK-3-5", "BOX", "Mountain Fastener Supply", "Hardware", 18.00, 29.99, 5.0, 40, 100},
{"Joist Hanger 2x6", "HANGER-26", "PCS", "Okanagan Hardware Distribution", "Hardware", 0.80, 1.45, 0.5, 200, 500},
{"Joist Hanger 2x8", "HANGER-28", "PCS", "Okanagan Hardware Distribution", "Hardware", 0.95, 1.65, 0.6, 150, 400},
{"Joist Hanger 2x10", "HANGER-210", "PCS", "Okanagan Hardware Distribution", "Hardware", 1.10, 1.85, 0.7, 100, 300},
{"Hurricane Tie H1", "TIE-H1", "PCS", "Okanagan Hardware Distribution", "Hardware", 0.65, 1.15, 0.3, 200, 500},
{"Simpson Strong-Tie LUS28", "SIMP-LUS28", "PCS", "Okanagan Hardware Distribution", "Hardware", 0.90, 1.50, 0.4, 150, 400},
{"Face Mount Joist Hanger 2x8", "HANGER-28FM", "PCS", "Okanagan Hardware Distribution", "Hardware", 0.90, 1.50, 0.4, 150, 400},
{"Architectural Shingles 30yr", "RF-ARCH-30", "BUNDLE", "BC Roofing Wholesale", "Roofing", 28.00, 42.00, 70.0, 50, 100},
{"Architectural Shingles (Black)", "RF-SH-BLK", "BUNDLE", "BC Roofing Wholesale", "Roofing", 28.00, 42.00, 70.0, 50, 100},
{"Architectural Shingles (Weathered Wood)", "RF-SH-WW", "BUNDLE", "BC Roofing Wholesale", "Roofing", 28.00, 42.00, 70.0, 50, 100},
{"Roofing Felt #15", "RF-FELT-15", "RL", "BC Roofing Wholesale", "Roofing", 15.00, 22.50, 15.0, 30, 80},
{"Ice & Water Shield 65'", "RF-ICE-65", "RL", "BC Roofing Wholesale", "Roofing", 65.00, 98.00, 36.0, 20, 50},
{"Ice and Water Barrier 65'", "RF-ICE-65", "RL", "BC Roofing Wholesale", "Roofing", 65.00, 98.00, 36.0, 20, 50},
{"Roof Edge Drip 10'", "RF-DRIP-10", "PCS", "BC Roofing Wholesale", "Roofing", 4.50, 7.50, 2.0, 60, 150},
{"Ridge Vent 4'", "RF-RIDGE-4", "PCS", "BC Roofing Wholesale", "Roofing", 8.00, 13.50, 3.0, 40, 80},
{"Starter Strip", "RF-START", "PCS", "BC Roofing Wholesale", "Roofing", 6.00, 10.00, 2.5, 40, 80},
{"1.25\" Roofing Nails 2.5kg", "RF-NAIL-125", "BOX", "BC Roofing Wholesale", "Roofing", 12.00, 19.00, 5.0, 30, 60},
{"Step Flashing", "RF-FLASH-STEP", "PCS", "BC Roofing Wholesale", "Roofing", 2.50, 4.50, 0.5, 60, 120},
{"Pipe Boot Flashing", "RF-FLASH-PIPE", "PCS", "BC Roofing Wholesale", "Roofing", 8.00, 14.00, 1.5, 20, 40},
{"Ice & Water Shield", "RF-ICE-WTR", "RL", "BC Roofing Wholesale", "Roofing", 65.00, 98.00, 36.0, 20, 50},
{"Ice and Water Barrier", "RF-ICE-WTR", "RL", "BC Roofing Wholesale", "Roofing", 65.00, 98.00, 36.0, 20, 50},
{"Roof Edge Drip 10'", "RF-EDGE-WHT", "PCS", "BC Roofing Wholesale", "Roofing", 4.50, 7.50, 2.0, 60, 150},
{"R-14 Fiberglass Batts 15x93", "INS-R13-15", "BAG", "Cariboo Insulation Ltd", "Insulation", 45.00, 68.00, 32.0, 30, 60},
{"R-20 Fiberglass Batts 15x93", "INS-R19-15", "BAG", "Cariboo Insulation Ltd", "Insulation", 55.00, 82.00, 42.0, 25, 50},
Expand All @@ -506,15 +506,15 @@ func Run() {
{"Flashing J 6 X 6 X 10", "CORN2006", "EA", "Gable Building Supply", "Cornice", 15.50, 23.25, 3.0, 20, 50},
{"Flashing Z Bar 3/4 X 10'", "CORN2009", "EA", "Gable Building Supply", "Cornice", 3.50, 5.25, 1.5, 30, 60},
{"Silicone Clear Caulk", "CORNCLEAR", "EA", "Okanagan Hardware Distribution", "Cornice", 5.75, 8.65, 0.8, 40, 80},
{"Cemtrim Textured 7/16 X 4 X 12", "CORNCTRM412+", "EA", "Gable Building Supply", "Cornice", 7.54, 11.31, 12.0, 50, 120},
{"Cemtrim Textured 7/16 X 6 X 12", "CORNCTRMG12+", "EA", "Gable Building Supply", "Cornice", 11.94, 17.91, 18.0, 30, 80},
{"Composite Trim Textured 7/16 X 4 X 12", "CORNCTRM412+", "EA", "Gable Building Supply", "Cornice", 7.54, 11.31, 12.0, 50, 120},
{"Composite Trim Textured 7/16 X 6 X 12", "CORNCTRMG12+", "EA", "Gable Building Supply", "Cornice", 11.94, 17.91, 18.0, 30, 80},
{"Vinyl H Mold 1/4 X 12'", "CORNHMOLD14", "EA", "Gable Building Supply", "Cornice", 4.00, 6.00, 1.0, 30, 60},
{"Poly Black 18 X 300", "CORNPOLY18", "RL", "Okanagan Hardware Distribution", "Cornice", 23.12, 34.68, 25.0, 10, 25},
{"Solid Soffit Hardie Textured 1/4 X 12 X 12", "CORNSFT1212", "EA", "Gable Building Supply", "Cornice", 15.25, 22.88, 20.0, 30, 60},
{"Solid Soffit Hardie Textured 1/4 X 16 X 12", "CORNSFT1612", "EA", "Gable Building Supply", "Cornice", 20.45, 30.68, 28.0, 20, 50},
{"Vented Soffit Hardie Textured 1/4 X 16 X 12", "CORNSFT1612V", "EA", "Gable Building Supply", "Cornice", 26.15, 39.23, 30.0, 25, 60},
{"Vented Soffit Hardie Textured 1/4 X 24 X 8", "CORNSFT2408V", "EA", "Gable Building Supply", "Cornice", 17.50, 26.25, 22.0, 20, 50},
{"Sheathing 1/8 X 4 X 9 (Green) NSP DRYLine TSX", "CORNSHTGR49", "EA", "Gable Building Supply", "Cornice", 9.36, 14.04, 24.0, 40, 100},
{"Solid Soffit Fibre Cement Textured 1/4 X 12 X 12", "CORNSFT1212", "EA", "Gable Building Supply", "Cornice", 15.25, 22.88, 20.0, 30, 60},
{"Solid Soffit Fibre Cement Textured 1/4 X 16 X 12", "CORNSFT1612", "EA", "Gable Building Supply", "Cornice", 20.45, 30.68, 28.0, 20, 50},
{"Vented Soffit Fibre Cement Textured 1/4 X 16 X 12", "CORNSFT1612V", "EA", "Gable Building Supply", "Cornice", 26.15, 39.23, 30.0, 25, 60},
{"Vented Soffit Fibre Cement Textured 1/4 X 24 X 8", "CORNSFT2408V", "EA", "Gable Building Supply", "Cornice", 17.50, 26.25, 22.0, 20, 50},
{"Sheathing 1/8 X 4 X 9 (Green) NSP Weather Barrier", "CORNSHTGR49", "EA", "Gable Building Supply", "Cornice", 9.36, 14.04, 24.0, 40, 100},
{"Window Tape 6\" (100')", "CORNTAPE", "RL", "Okanagan Hardware Distribution", "Cornice", 22.00, 33.00, 3.0, 15, 30},

// Random-length / economy grade
Expand Down Expand Up @@ -1087,7 +1087,7 @@ func Run() {
var podURL, podSigner *string
var podTS *time.Time
if dStatus == "DELIVERED" {
u := "https://storage.gable.com/pod/" + uuid.New().String() + ".jpg"
u := "https://storage.gablebuildingsupply.example/pod/" + uuid.New().String() + ".jpg"
n := "Site Foreman"
t := sDate.Add(time.Duration(8+rand.Intn(6)) * time.Hour)
podURL = &u
Expand Down
10 changes: 5 additions & 5 deletions core/internal/characterization/testdata/goldens/dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"alert_type": "LOW_STOCK",
"current_qty": 12,
"location_id": "<id-2>",
"name": "Sheathing 1/8 X 4 X 9 (Green) NSP DRYLine TSX",
"name": "Sheathing 1/8 X 4 X 9 (Green) NSP Weather Barrier",
"product_id": "<id-4>",
"reorder_qty": 40,
"sku": "CORNSHTGR49"
Expand All @@ -39,10 +39,10 @@
"alert_type": "LOW_STOCK",
"current_qty": 12,
"location_id": "<id-2>",
"name": "Simpson Strong-Tie LUS28",
"name": "Face Mount Joist Hanger 2x8",
"product_id": "<id-5>",
"reorder_qty": 150,
"sku": "SIMP-LUS28"
"sku": "HANGER-28FM"
},
{
"alert_type": "LOW_STOCK",
Expand Down Expand Up @@ -75,7 +75,7 @@
"alert_type": "LOW_STOCK",
"current_qty": 17,
"location_id": "<id-2>",
"name": "Vented Soffit Hardie Textured 1/4 X 24 X 8",
"name": "Vented Soffit Fibre Cement Textured 1/4 X 24 X 8",
"product_id": "<id-9>",
"reorder_qty": 20,
"sku": "CORNSFT2408V"
Expand All @@ -93,7 +93,7 @@
"alert_type": "LOW_STOCK",
"current_qty": 17,
"location_id": "<id-2>",
"name": "Ice & Water Shield 65'",
"name": "Ice and Water Barrier 65'",
"product_id": "<id-11>",
"reorder_qty": 20,
"sku": "RF-ICE-65"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@
"lines": [
{
"customer_note": "",
"description": "5/8 Drywall Firecode 4x8",
"description": "5/8 Drywall Type X 4x8",
"id": "<id-37>",
"is_special_order": false,
"line_total": 819,
Expand Down Expand Up @@ -847,7 +847,7 @@
},
{
"customer_note": "",
"description": "Vented Soffit Hardie Textured 1/4 X 24 X 8",
"description": "Vented Soffit Fibre Cement Textured 1/4 X 24 X 8",
"id": "<id-59>",
"is_special_order": false,
"line_total": 2703.75,
Expand All @@ -873,7 +873,7 @@
},
{
"customer_note": "",
"description": "Cemtrim Textured 7/16 X 6 X 12",
"description": "Composite Trim Textured 7/16 X 6 X 12",
"id": "<id-62>",
"is_special_order": false,
"line_total": 1504.44,
Expand Down Expand Up @@ -919,7 +919,7 @@
},
{
"customer_note": "",
"description": "Vented Soffit Hardie Textured 1/4 X 16 X 12",
"description": "Vented Soffit Fibre Cement Textured 1/4 X 16 X 12",
"id": "<id-67>",
"is_special_order": false,
"line_total": 3687.62,
Expand All @@ -932,7 +932,7 @@
},
{
"customer_note": "",
"description": "Vented Soffit Hardie Textured 1/4 X 16 X 12",
"description": "Vented Soffit Fibre Cement Textured 1/4 X 16 X 12",
"id": "<id-69>",
"is_special_order": false,
"line_total": 274.61,
Expand All @@ -945,7 +945,7 @@
},
{
"customer_note": "",
"description": "Sheathing 1/8 X 4 X 9 (Green) NSP DRYLine TSX",
"description": "Sheathing 1/8 X 4 X 9 (Green) NSP Weather Barrier",
"id": "<id-70>",
"is_special_order": false,
"line_total": 463.32,
Expand Down
Loading
Loading