91 lines
3.4 KiB
Go
91 lines
3.4 KiB
Go
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
|
|
}
|