*/ private const ADDRESS_KEYS = [ 'name', 'address_street_1', 'address_street_2', 'city', 'state', 'country_id', 'zip', 'phone', 'fax', ]; /** * The portal guard has already vetted the caller, and a contact is only * ever handed its own record, so there is nothing further to check. */ public function authorize(): bool { return true; } /** * Constraints applied to the submitted profile patch. */ public function rules(): array { $rules = [ 'name' => ['nullable'], 'password' => ['nullable', 'min:8'], 'email' => ['nullable', new IdnEmail, $this->emailIsFree()], ]; foreach (['billing', 'shipping'] as $block) { foreach (self::ADDRESS_KEYS as $key) { $rules["{$block}.{$key}"] = ['nullable']; } } $rules['customer_avatar'] = ['nullable', 'file', 'mimes:gif,jpg,png', 'max:20000']; $rules['is_customer_avatar_removed'] = ['nullable', 'boolean']; return $rules; } /** * The profile columns the controller may hand straight to the model. * * @return array */ public function customerAttributes(): array { return $this->safe()->only([ 'name', 'email', 'password', ]); } /** * The submitted shipping block, or null when none was sent. * * @return array|null */ public function shippingAddress(): ?array { return $this->addressBlock('shipping', Address::SHIPPING_TYPE); } /** * The submitted billing block, or null when none was sent. * * @return array|null */ public function billingAddress(): ?array { return $this->addressBlock('billing', Address::BILLING_TYPE); } /** * No two contacts of the same company may share an address. * * Both operands are read from the ambient request exactly as they always * have been: the tenant comes from the `company` request header (which the * slug-scoped portal routes do not send) and the row to skip comes from the * default guard rather than the portal one. Left untouched deliberately. */ private function emailIsFree(): Unique { return Rule::unique('customers') ->where('company_id', $this->header('company')) ->ignore(auth()->id(), 'id'); } /** * Lift one address block off the payload and stamp its type onto it. * * Anything that did not arrive as an array - absent, null, a scalar - comes * back as null, which is how the caller tells "replace this address" apart * from "leave this address alone". * * @return array|null */ private function addressBlock(string $block, string $type): ?array { $submitted = $this->input($block); if (! is_array($submitted)) { return null; } return collect($submitted) ->merge(['type' => $type]) ->toArray(); } }