PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 trunk 1.2.0 All 47 releases
fluent-cart / app / Services / Filter / CustomerFilter.php

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

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