updates on the backend about the reverse logistics
This commit is contained in:
@@ -1,56 +1,76 @@
|
||||
package constants
|
||||
|
||||
import "strings"
|
||||
|
||||
// What a client delivers, and what that implies operationally.
|
||||
//
|
||||
// ─── One vocabulary, not two ───────────────────────────────────────────────
|
||||
//
|
||||
// These eight values already existed as `validCategories` in
|
||||
// controllers/doormilePricingController.go and as the comment on
|
||||
// models.DoormilePricing.Category. Tenants now carry one too, so the set is
|
||||
// promoted here rather than copied: a second list would drift, and a tenant
|
||||
// whose category is not a pricing category cannot be priced.
|
||||
// Categories driven by pricing, onboarding, and logistics.
|
||||
// Non-perishable goods like Steel, Clothing, Electronics, Automotive have return paths.
|
||||
// Perishable goods like Food, Meat, and Fresh Produce have NO return option.
|
||||
var DeliveryCategories = map[string]bool{
|
||||
"General": true,
|
||||
"Documents": true,
|
||||
"Electronics": true,
|
||||
"Clothing": true,
|
||||
"Fragile": true,
|
||||
"Medical": true,
|
||||
"Automotive": true,
|
||||
"Food": true,
|
||||
"General": true,
|
||||
"Documents": true,
|
||||
"Electronics": true,
|
||||
"Clothing": true,
|
||||
"Fragile": true,
|
||||
"Medical": true,
|
||||
"Automotive": true,
|
||||
"Steel": true,
|
||||
"Food": true,
|
||||
"Meat": true,
|
||||
"FreshProduce": true,
|
||||
}
|
||||
|
||||
// DeliveryCategoryList is the same set, ordered, for anything that renders a
|
||||
// choice. General first because it is the safe default; Food last because it
|
||||
// is the one that turns a capability off.
|
||||
// DeliveryCategoryList is the same set, ordered, for anything that renders a choice.
|
||||
var DeliveryCategoryList = []string{
|
||||
"General", "Documents", "Electronics", "Clothing",
|
||||
"Fragile", "Medical", "Automotive", "Food",
|
||||
"Fragile", "Medical", "Automotive", "Steel", "Food", "Meat", "FreshProduce",
|
||||
}
|
||||
|
||||
// CategoryDefault is what a client with no category recorded is treated as.
|
||||
// Every tenant onboarded before this field existed has an empty string, and
|
||||
// they must keep behaving exactly as they did — which means reverse logistics
|
||||
// stays available to them.
|
||||
const CategoryDefault = "General"
|
||||
|
||||
// ReverseLogisticsAllowed reports whether a return journey makes sense for
|
||||
// what this client ships.
|
||||
//
|
||||
// Food is the exception: a meal that comes back is waste, not inventory. There
|
||||
// is nothing to restock, nothing to refund against a returned item, and a
|
||||
// rider carrying it to a hub is carrying rubbish. Offering RTO there is not a
|
||||
// harmless extra button — it invites an operator to start a return journey
|
||||
// that can only end in disposal, and it puts a return charge on a client's
|
||||
// invoice for a parcel nobody can resell.
|
||||
//
|
||||
// Everything else can come back: clothing is the canonical case (wrong size),
|
||||
// and electronics, documents and automotive parts all have a real return path.
|
||||
//
|
||||
// An UNKNOWN or empty category allows returns. That is deliberate: this field
|
||||
// is new, every existing tenant has no value for it, and a default of "off"
|
||||
// would silently withdraw a working capability from every client already using
|
||||
// it. New information must not change old behaviour.
|
||||
func ReverseLogisticsAllowed(category string) bool {
|
||||
return category != "Food"
|
||||
// NonReturnableCategories defines categories of goods that cannot be returned
|
||||
// (Food items, Meat items, Fresh produce/perishables). Returning them causes
|
||||
// spoilage and health risks. Non-perishables like Steel, Clothing, Electronics, etc. can be returned.
|
||||
var NonReturnableCategories = map[string]bool{
|
||||
"food": true,
|
||||
"meat": true,
|
||||
"raw meat": true,
|
||||
"fresh produce": true,
|
||||
"freshproduce": true,
|
||||
"fresh": true,
|
||||
"perishable": true,
|
||||
"fish": true,
|
||||
"seafood": true,
|
||||
"poultry": true,
|
||||
"dairy": true,
|
||||
"vegetables": true,
|
||||
"fruits": true,
|
||||
}
|
||||
|
||||
// IsPerishableCategory checks if a category or item description is perishable (food, meat, fresh produce).
|
||||
func IsPerishableCategory(category string) bool {
|
||||
cat := strings.ToLower(strings.TrimSpace(category))
|
||||
if cat == "" {
|
||||
return false
|
||||
}
|
||||
if NonReturnableCategories[cat] {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(cat, "food") || strings.Contains(cat, "meat") || strings.Contains(cat, "fresh") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ReverseLogisticsAllowed reports whether a return journey makes sense for
|
||||
// what this client or parcel ships.
|
||||
//
|
||||
// Food items, Meat items, and Fresh produce CANNOT be returned.
|
||||
// Products like Steel, Electronics, Clothing, and Automotive CAN be returned.
|
||||
func ReverseLogisticsAllowed(category string) bool {
|
||||
return !IsPerishableCategory(category)
|
||||
}
|
||||
|
||||
@@ -2,29 +2,26 @@ package constants
|
||||
|
||||
import "testing"
|
||||
|
||||
// The rule the whole feature exists for: a returned meal is waste, not
|
||||
// inventory, so Food clients get no reverse-logistics path.
|
||||
func TestFoodIsTheOnlyCategoryWithoutReturns(t *testing.T) {
|
||||
if ReverseLogisticsAllowed("Food") {
|
||||
t.Error("Food allows returns; a returned meal can only be disposed of")
|
||||
}
|
||||
for _, c := range DeliveryCategoryList {
|
||||
if c == "Food" {
|
||||
continue
|
||||
// Food, Meat, and Fresh produce items must NOT have return options.
|
||||
// Durable goods like Steel, Electronics, Clothing, etc. DO have return options.
|
||||
func TestPerishableCategoriesDoNotAllowReturns(t *testing.T) {
|
||||
for _, nonRet := range []string{"Food", "Meat", "FreshProduce", "raw meat", "fish", "fresh vegetables"} {
|
||||
if ReverseLogisticsAllowed(nonRet) {
|
||||
t.Errorf("Perishable category %q should NOT allow returns", nonRet)
|
||||
}
|
||||
if !ReverseLogisticsAllowed(c) {
|
||||
t.Errorf("%s does not allow returns; only Food should be excluded", c)
|
||||
}
|
||||
|
||||
for _, ret := range []string{"Steel", "Clothing", "Electronics", "Automotive", "General", "Documents"} {
|
||||
if !ReverseLogisticsAllowed(ret) {
|
||||
t.Errorf("Durable category %q SHOULD allow returns", ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New information must not change old behaviour. Every tenant onboarded before
|
||||
// this field existed has an empty category, and they were all using returns.
|
||||
func TestUnknownAndEmptyCategoriesKeepReturns(t *testing.T) {
|
||||
for _, c := range []string{"", " ", "Groceries", "SomethingNew"} {
|
||||
func TestUnknownAndEmptyCategoriesDefaultToAllowed(t *testing.T) {
|
||||
for _, c := range []string{"", " ", "SomethingNew"} {
|
||||
if !ReverseLogisticsAllowed(c) {
|
||||
t.Errorf("category %q withdrew returns; an unrecognised category must not "+
|
||||
"silently remove a capability an existing client is already using", c)
|
||||
t.Errorf("category %q withdrew returns; empty/unknown should default open", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user