PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Services / Filter / CustomerFilter.php

CustomerFilter.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.1, at app/Services/Filter/CustomerFilter.php

286 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Filter;
4
5 use FluentCart\App\Helpers\AddressHelper;
6 use FluentCart\App\Models\Activity;
7 use FluentCart\App\Models\Customer;
8 use FluentCart\Framework\Support\Arr;
9 use FluentCart\Framework\Support\Str;
10
11 class CustomerFilter extends BaseFilter
12 {
13
14 public function applySimpleFilter(?string $search = null): void
15 {
16 $isApplied = $this->applySimpleOperatorFilter($search);
17 if ($isApplied) {
18 return;
19 }
20 $this->query->when($search ?? $this->search, function ($query, $search) {
21
22 return $query
23 ->where(function ($query) use ($search) {
24 $search = trim($search);
25 $searchLike = addcslashes($search, '\\%_');
26 $query
27 ->whereRaw("CONCAT(first_name, ' ', last_name) LIKE ?", ["%{$searchLike}%"])
28 ->when(is_numeric($search), function ($query) use ($search) {
29 $query->orWhere('id', 'LIKE', "%{$search}%");
30 })
31 ->when(!str_contains($search, ' '), function ($query) use ($search) {
32 $query->orWhere('email', 'LIKE', "%{$search}%");
33 });
34 });
35 });
36 }
37
38
39 public function tabsMap(): array
40 {
41 return [
42
43 ];
44 }
45
46 public function getModel(): string
47 {
48 return Customer::class;
49 }
50
51 public static function getFilterName(): string
52 {
53 return 'customers';
54 }
55
56 /**
57 * SCREEN key — `admin_customer_search` is the one screen on `GET /customers`
58 * that sends a `with` at all. CustomerTable.js and ChangeOrderCustomer.vue
59 * send none.
60 *
61 * PUBLIC keys — the address sets and the labels are relation names an
62 * external consumer can reasonably ask a customer endpoint for, and they
63 * were accepted values before this allow-list existed. They are restored as
64 * first-class entry points rather than dropped: "no consumer in our repos"
65 * is not the same as "no consumer", and silently narrowing a published
66 * response is the exact regression this tier exists to prevent.
67 *
68 * `GET /customers/{id}` is a different mechanism entirely —
69 * CustomerController::allowedEagerLoads() — and keeps its own bars. This
70 * map governs the list route only.
71 *
72 * Deliberately NOT allowlisted: `wpUser`, which is a BelongsTo onto the
73 * WordPress users table. It must stay out of this map on its own merit —
74 * the `User::$hidden` fix is a second line of defence, not a licence to
75 * expose the relation. `orders` and `subscriptions` stay off too: they are
76 * billing rows behind other permissions.
77 *
78 * @return array<string, callable>
79 */
80 protected function allowedWiths(): array
81 {
82 return [
83 'admin_customer_search' => [$this, 'adminCustomerSearch'],
84
85 'shipping_address' => [$this, 'publicShippingAddress'],
86 'billing_address' => [$this, 'publicBillingAddress'],
87 'primary_shipping_address' => [$this, 'publicPrimaryShippingAddress'],
88 'primary_billing_address' => [$this, 'publicPrimaryBillingAddress'],
89 'labels' => [$this, 'publicLabels'],
90 ];
91 }
92
93 /**
94 * `GET /customers`, sent by OrderCustomerInformation.vue when an admin
95 * searches for a customer to attach to an order. The screen prefills both
96 * address blocks, so both sets are loaded in ONE `with()` call — a second
97 * entry issuing its own `with()` would array_merge over this one.
98 *
99 * No further permission bar: the route already requires `customers/view`
100 * and these are the customer's own addresses, which is precisely what that
101 * permission grants.
102 *
103 * No select: CustomerAddresses::$appends is `formatted_address`,
104 * `company_name`, `vat_number` and `legal_registration_id`, and the last
105 * three read stored meta off the loaded row.
106 *
107 * @param \FluentCart\Framework\Database\Orm\Builder $query
108 * @return \FluentCart\Framework\Database\Orm\Builder
109 */
110 protected function adminCustomerSearch($query)
111 {
112 return $query->with([
113 'shipping_address',
114 'billing_address',
115 ]);
116 }
117
118 /**
119 * `with[]=shipping_address` — a public entry point. No bar beyond the
120 * route's own `customers/view`: a customer's addresses are customer data.
121 *
122 * @param \FluentCart\Framework\Database\Orm\Builder $query
123 * @return \FluentCart\Framework\Database\Orm\Builder
124 */
125 protected function publicShippingAddress($query)
126 {
127 return $query->with(['shipping_address']);
128 }
129
130 /**
131 * `with[]=billing_address` — a public entry point, covered by the route's
132 * own `customers/view` for the same reason.
133 *
134 * @param \FluentCart\Framework\Database\Orm\Builder $query
135 * @return \FluentCart\Framework\Database\Orm\Builder
136 */
137 protected function publicBillingAddress($query)
138 {
139 return $query->with(['billing_address']);
140 }
141
142 /**
143 * `with[]=primary_shipping_address` — a public entry point. The primary row
144 * is a HasOne narrowing of the same table the route already grants.
145 *
146 * @param \FluentCart\Framework\Database\Orm\Builder $query
147 * @return \FluentCart\Framework\Database\Orm\Builder
148 */
149 protected function publicPrimaryShippingAddress($query)
150 {
151 return $query->with(['primary_shipping_address']);
152 }
153
154 /**
155 * `with[]=primary_billing_address` — a public entry point, covered by the
156 * route's own `customers/view` for the same reason.
157 *
158 * @param \FluentCart\Framework\Database\Orm\Builder $query
159 * @return \FluentCart\Framework\Database\Orm\Builder
160 */
161 protected function publicPrimaryBillingAddress($query)
162 {
163 return $query->with(['primary_billing_address']);
164 }
165
166 /**
167 * `with[]=labels` — a public entry point, and the one on this map that is
168 * NOT covered by the route's own permission. Labels are their own resource
169 * with their own `labels/view` bar, which is what
170 * CustomerController::allowedEagerLoads() applies on the detail route; the
171 * list route states the same bar here so the two doors agree.
172 *
173 * @param \FluentCart\Framework\Database\Orm\Builder $query
174 * @return bool|\FluentCart\Framework\Database\Orm\Builder
175 */
176 protected function publicLabels($query)
177 {
178 if (!$this->userCanAny('labels/view')) {
179 return false;
180 }
181
182 return $query->with(['labels']);
183 }
184
185 public function applyActiveViewFilter(?string $activeView = null): void
186 {
187
188 }
189
190 public static function getSearchableFields(): array
191 {
192 return [
193 'id' => [
194 'column' => 'ID',
195 'description' => 'Customer ID',
196 'type' => 'numeric'
197 ]
198 ];
199 }
200
201 public static function advanceFilterOptions(): array
202 {
203 $filters = [
204 'order' => [
205 'label' => __('Order Property', 'fluent-cart'),
206 'value' => 'order',
207 'children' => [
208 [
209 'label' => __('By Order Items', 'fluent-cart'),
210 'value' => 'order_items',
211 'column' => 'object_id',
212 'filter_type' => 'relation',
213 'relation' => 'orders.order_items',
214 'remote_data_key' => 'product_variations',
215 'type' => 'remote_tree_select',
216 'limit' => 10,
217 ],
218 [
219 'label' => __('Purchases', 'fluent-cart'),
220 'value' => 'purchase_count',
221 'type' => 'numeric',
222 ],
223 [
224 'label' => __('First Purchase Date', 'fluent-cart'),
225 'value' => 'first_purchase_date',
226 'type' => 'dates',
227 'filter_type' => 'date',
228 ],
229 [
230 'label' => __('Last Purchase Date', 'fluent-cart'),
231 'value' => 'last_purchase_date',
232 'type' => 'dates',
233 'filter_type' => 'date',
234 ]
235 ],
236 ],
237 'customer' => [
238 'label' => __('Customer Property', 'fluent-cart'),
239 'value' => 'customer',
240 'children' => [
241 [
242 'label' => __('Customer Name', 'fluent-cart'),
243 'value' => 'customer_full_name',
244 'type' => 'text',
245 'filter_type' => 'custom',
246 'operators' => [
247 'like_all' => __('Contains', 'fluent-cart'),
248 'starts_with' => __('Starts With', 'fluent-cart'),
249 'ends_with' => __('Ends With', 'fluent-cart'),
250 'not_like' => __('Not Contains', 'fluent-cart'),
251 ],
252 'callback' => function ($query, $data) {
253 $query->searchByFullName($data);
254 }
255 ],
256 [
257 'label' => __('Customer Email', 'fluent-cart'),
258 'value' => 'email',
259 'type' => 'text',
260 'filter_type' => 'column',
261 'column' => 'email',
262 ],
263 [
264 'label' => __('Customer LTV', 'fluent-cart'),
265 'value' => 'ltv',
266 'type' => 'numeric',
267 'filter_type' => 'column',
268 'column' => 'ltv'
269 ]
270 ],
271 ]
272 ];
273 return LabelFilter::advanceFilterOptionsForOther($filters);
274 }
275
276 public function dateColumns(): array
277 {
278 return array_merge(parent::dateColumns(), ['first_purchase_date', 'last_purchase_date']);
279 }
280
281 public function centColumns(): array
282 {
283 return ['ltv'];
284 }
285 }
286