package services import ( "errors" "strings" ) // What a merchant is allowed to change about their own business. // // An allowlist, and it has to be one. The obvious implementation — hand the // parsed body to GORM's `Updates` — would let anyone who can reach the endpoint // set `approved`, `status`, `partnerid`, `partneruserid`, `moduleid`, // `configid` or `tenanttoken` on their own record: approve themselves onto the // platform, move themselves under another partner, or reassign their billing. // None of those belong to the merchant, and none of them are things a UI would // ever send, which is exactly what makes the omission easy to miss. // // So the fields are named here, once, and the repository writes nothing it is // not given. Anything absent from this map is untouched rather than blanked — // a profile form that renders four fields must not erase the twenty it did not. // // `tenantid` is deliberately absent too: it identifies the row, it is never a // value to be written. var editableTenantFields = map[string]bool{ // What a shopper sees. "tenantname": true, "tenantimage": true, "tenantinfo": true, // The kind of shop it is. Omitting it was a silent data loss: store setup // marks Store type REQUIRED, sends it on save, and the merchant is shown a // successful step while the answer is dropped here. It describes the // business rather than its standing on the platform, so it belongs to the // merchant in the way "approved" and "partnerid" do not. "tenanttype": true, // How to reach the business. "primaryemail": true, "primarycontact": true, "companyname": true, // The person who administers the shop — the Store admin. // // `tenants.firstname` — there is no lastname column, so this is the whole // name. It was collected nowhere and writable nowhere, which is why every // merchant read so far comes back with it empty and the store profile shows // "—" for Store admin. It describes the business's own contact person, in // the same way `companyname` does, and belongs to the merchant rather than // to the platform. "firstname": true, // Where it is. "address": true, "suburb": true, "city": true, "state": true, "postcode": true, "latitude": true, "longitude": true, // Legal and trading terms. "registrationno": true, "licenseno": true, "minorder": true, "subcategoryid": true, } // TenantProfileUpdate reduces a request to the fields a merchant may set. // // Returns an error rather than an empty map when nothing survives: a write that // changes nothing and reports success is indistinguishable from one that // worked, and the caller would go on believing their licence number was saved. func TenantProfileUpdate(fields map[string]any) (map[string]any, error) { clean := make(map[string]any, len(fields)) for key, value := range fields { lower := strings.ToLower(strings.TrimSpace(key)) if !editableTenantFields[lower] { continue } // A blank string is "not supplied", not "erase it". The profile form // sends every field it renders on every save, so honouring blanks would // let a half-filled form wipe an address somebody typed last month. if text, ok := value.(string); ok && strings.TrimSpace(text) == "" { continue } clean[lower] = value } if len(clean) == 0 { return nil, errors.New("nothing to update — no editable field was supplied") } return clean, nil }