Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.96% covered (success)
97.96%
240 / 245
78.57% covered (warning)
78.57%
11 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConversionController
97.96% covered (success)
97.96%
240 / 245
78.57% covered (warning)
78.57%
11 / 14
81
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 postConversion
100.00% covered (success)
100.00%
47 / 47
100.00% covered (success)
100.00%
1 / 1
36
 getConversionsDigest
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
1 / 1
18
 getConversion
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getConversionTransactions
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 postConversionApprove
94.87% covered (success)
94.87%
37 / 39
0.00% covered (danger)
0.00%
0 / 1
5.00
 postConversionReject
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 postConversionFlag
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 postConversionNote
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
3
 __validateNoteRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 __validateConversionRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 __validateYYYYMMDD
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 __validateConversionApprovalRequest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getGoogleCampaignIds
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
1.04
1<?php
2
3namespace App\Http\Controllers\api;
4
5use DB;
6use App\Http\Controllers\Controller;
7use Illuminate\Http\Request;
8use Illuminate\Http\JsonResponse;
9use Illuminate\Database\Eloquent\Builder;
10
11use App\Models\Role;
12use App\Models\Conversion;
13use App\Models\ConversionNote;
14use App\Models\ConversionDigest;
15use App\Models\ConversionStatus;
16use App\Models\GoogleAdsCampaign;
17use App\Models\GoogleAdsCustomer;
18use App\Models\ConversionCurrency;
19use App\Models\ConversionTransaction;
20
21use App\Libs\GoogleAds;
22use App\Libs\SqlFormatter;
23
24use Carbon\Carbon;
25
26use App\Traits\GoogleTrait;
27
28class ConversionController extends BaseController
29{
30    use GoogleTrait;
31
32    protected GoogleAds $googleAds;
33
34    /**
35     * Constructore
36     */
37    public function __construct(\App\Libs\GoogleAds $ga) {
38        $this->googleAds = $ga;
39    }
40
41    /**
42     * Creates one conversion record
43     *
44     * @author gbh
45     * @see http://swagger-humanverify.fruitbat.systems/#post-/api/conversions
46     */
47    public function postConversion(Request $request):JsonResponse
48    {
49        //$this->uploadOfflineConversion();
50        //return $this->sendResponse(201, "woozozo");
51
52        /**
53         * Validate request body
54         * HTTP 400
55         */
56        $validationTest = $this->__validateConversionRequest($request);
57        if (is_object($validationTest)) {
58            return $validationTest;
59        }
60
61        /**
62         * Get status id
63         */
64        $statusId = ConversionStatus::where('name', '=', 'new')->first()->id;
65
66        /**
67         * Figure out the conversion currency
68         */
69        $conversionCurrencyId = 1;
70        if (ConversionCurrency::where('name', '=', $request->conversion_currency)->count() > 0) {
71            $conversionCurrencyId = ConversionCurrency::where('name', '=', $request->conversion_currency)->first()->id;
72        }
73
74        /**
75         * Create the conversion
76         */
77        $conversion = new Conversion();
78
79        // mandatory and defined fields
80        $conversion->website_url = $request->website_url;
81        $conversion->status_id = $statusId;
82        $conversion->mobile = isset($request->mobile) ? (bool)$request->mobile : false;
83        $conversion->conversion_currency = $conversionCurrencyId;
84        $conversion->conversion_time = date("Y-m-d H:i:s");
85
86        // optional strings fields
87        $conversion->conversion_value = isset($request->conversion_value) ? (string)$request->conversion_value : null;
88        $conversion->gclid = isset($request->gclid) ? (string)$request->gclid : null;
89        $conversion->fbclid = isset($request->fbclid) ? (string)$request->fbclid : null;
90        $conversion->event_category = isset($request->event_category) ? (string)$request->event_category : null;
91        $conversion->event_action = isset($request->event_action) ? (string)$request->event_action : null;
92        $conversion->event_label = isset($request->event_label) ? (string)$request->event_label : null;
93        $conversion->utm_campaign_id = isset($request->utm_campaign_id) ? (string)$request->utm_campaign_id : null;
94        $conversion->utm_campaign_source = isset($request->utm_campaign_source) ? (string)$request->utm_campaign_source : null;
95        $conversion->utm_campaign_medium = isset($request->utm_campaign_medium) ? (string)$request->utm_campaign_medium : null;
96        $conversion->utm_campaign_name = isset($request->utm_campaign_name) ? (string)$request->utm_campaign_name : null;
97        $conversion->utm_campaign_term = isset($request->utm_campaign_term) ? (string)$request->utm_campaign_term : null;
98        $conversion->utm_campaign_content = isset($request->utm_campaign_content) ? (string)$request->utm_campaign_content : null;
99        $conversion->conversion_name = isset($request->conversion_name) ? (string)$request->conversion_name : null;
100        $conversion->nw = isset($request->nw) ? (string)$request->nw : null;
101        $conversion->cmpid = isset($request->cmpid) ? (string)$request->cmpid : null;
102        $conversion->agid = isset($request->agid) ? (string)$request->agid : null;
103        $conversion->fiid = isset($request->fiid) ? (string)$request->fiid : null;
104        $conversion->tgid = isset($request->tgid) ? (string)$request->tgid : null;
105        $conversion->loc = isset($request->loc) ? (string)$request->loc : null;
106        $conversion->ploc = isset($request->ploc) ? (string)$request->ploc : null;
107        $conversion->match = isset($request->match) ? (string)$request->match : null;
108        $conversion->dev = isset($request->dev) ? (string)$request->dev : null;
109        $conversion->devm = isset($request->devm) ? (string)$request->devm : null;
110        $conversion->cmpt = isset($request->cmpt) ? (string)$request->cmpt : null;
111        $conversion->ad = isset($request->ad) ? (string)$request->ad : null;
112        $conversion->pl = isset($request->pl) ? (string)$request->pl : null;
113        $conversion->pltar = isset($request->pltar) ? (string)$request->pltar : null;
114        $conversion->pr1 = isset($request->pr1) ? (string)$request->pr1 : null;
115        $conversion->pr2 = isset($request->pr2) ? (string)$request->pr2 : null;
116        $conversion->url = isset($request->url) ? (string)$request->url : null;
117        $conversion->adpos = isset($request->adpos) ? (string)$request->adpos : null;
118        $conversion->form_data = isset($request->form_data) ? (string)$request->form_data : null;
119        $conversion->save();
120
121        /**
122         * HTTP 201
123         */
124        return $this->sendResponse(201, null);
125    } // postConversion
126
127
128    /**
129     * Return one page of conversions
130     *
131     * query string 'status' value is one of:
132     *  - new
133     *  - approved
134     *  - rejected
135     *  - all *default
136     *
137     * query string 'order' value is one of:
138     *  - customer (sorts by name of customer)
139     *  - campaign (sorts by name of campaign)
140     *  - gclid
141     *  - fbclid
142     *  - *default created_at
143     *
144     *  query string args:
145     *      - customer_id
146     *      - campaign_id
147     *      - status (the name of the status)
148     *      - start
149     *      - end
150     *      - order
151     *      - sort
152     *      - page
153     *      - size
154     *
155     * @author gbh
156     * @see http://swagger.humanverify.test/#get-/api/conversions/digest
157     */
158    public function getConversionsDigest(Request $request):JsonResponse
159    {
160        /**
161         * Get pagination data from request
162         */
163        $paginationDataResult = $this->__validateAndExtractPaginationData($request);
164        $size = $paginationDataResult['size'];
165
166        /**
167         * Get filters, order and sort data from request
168         */
169        $order = in_array($request->query('order'), ['campaign', 'customer', 'gclid', 'fbclid']) ? $request->query('order') : 'created_at';
170        $sort = in_array(strtolower($request->query('sort')), ['asc', 'desc']) ? $request->query('sort') : 'desc';
171        $status = $request->query('status', null);
172        $filter_campaign_id = $request->query('campaign_id', null);
173        $filter_customer_id = $request->query('customer_id', null);
174
175        /**
176         * Function to yield start and end dates
177         *
178         * @param  Request  $request
179         * @return Generator
180         */
181        $getStartEndDates = function (Request $request) {
182            // default start and end dates
183            $startDate = (string)Carbon::createFromFormat('Y-m-d', '1980-01-01')->format('Y-m-d').' 00:00:01';
184            $endDate = (string)Carbon::now()->addDay()->format('Y-m-d').' 23:59:59';
185
186            // use provided dates if present and valid, otherwise use defaults
187            $start = $request->query('start', null);
188            $end = $request->query('end', null);
189
190            // validate and assign start date if non null
191            if (!is_null($start) && $this->__validateYYYYMMDD($start)) {
192                $startDate = (string)Carbon::createFromFormat('Y-m-d', $start)->format('Y-m-d').' 00:00:01';
193            }
194
195            // validate and assign end date if non null
196            if (!is_null($end) && $this->__validateYYYYMMDD($end)) {
197                $endDate = (string)Carbon::createFromFormat('Y-m-d', $end)->format('Y-m-d').' 23:59:59';
198            }
199
200            yield $startDate;
201            yield $endDate;
202        }; // getStartEndDates
203
204        /**
205         * Get start and end dates
206         */
207        $startEndDate =  $getStartEndDates($request);
208        $startDate = $startEndDate->current();
209        $startEndDate->next();
210        $endDate = $startEndDate->current();
211
212        /**
213         * Function to optionally filter query by status id from status name
214         *
215         * @param  Builder $conversionsPage The builder to add the status filter to
216         * @param  Request $request
217         * @return Builder
218         */
219        $filterByStatus = function (Builder $conversionsPage, Request $request) {
220            try {
221                $statusId = ConversionStatus::where('name', '=', $request->query('status'))->first()->id;
222                return $statusId ? $conversionsPage->where('status_id', '=', $statusId) : $conversionsPage;
223            } catch (\Exception $e) {
224                return $conversionsPage;
225            }
226        }; //filterByStatus
227
228
229        $googleAdsCustomers = $this->googleAds->getGoogleAdsCustomers();
230        $googleAdsCampaignIds = self::getGoogleCampaignIds($googleAdsCustomers, $request);
231        #return $this->sendResponse(200, $googleAdsCampaignIds);
232
233        /**
234         * Base query
235         */
236        $conversionsPage = ConversionDigest::leftJoin('google_ads_campaigns', 'google_ads_campaigns.campaign_id', '=', 'cmpid')
237            ->select(
238                'conversions.id',
239                'conversions.gclid',
240                'conversions.fbclid',
241                'conversions.website_url',
242                'conversions.cmpid',
243                'conversions.created_at',
244                'conversions.status_id',
245                'google_ads_customers.customer_id',
246                'google_ads_customers.customer_descriptive_name',
247                'google_ads_campaigns.campaign_id',
248                'google_ads_campaigns.campaign_name'
249            )
250            ->leftJoin('google_ads_customers', 'google_ads_customers.customer_id', '=', 'google_ads_campaigns.customer_id')
251            ->whereIn('cmpid', $googleAdsCampaignIds) // only return campaigns of the session user
252            ->where('conversions.created_at', '>', $startDate)
253            ->where('conversions.created_at', '<', $endDate);
254
255
256        /**
257         * Optional filters
258         */
259        $conversionsPage = $filter_customer_id ? $conversionsPage->where('google_ads_customers.customer_id', '=', $filter_customer_id) : $conversionsPage;
260        $conversionsPage = $filter_campaign_id ? $conversionsPage->where('google_ads_campaigns.campaign_id', '=', $filter_campaign_id) : $conversionsPage;
261        $conversionsPage = $request->query('gclid') ? $conversionsPage->where('gclid', '=', $request->query('gclid')) : $conversionsPage;
262        $conversionsPage = $request->query('fbclid') ? $conversionsPage->where('fbclid', '=', $request->query('fbclid')) : $conversionsPage;
263
264        /**
265         * Optional filter by status name
266         */
267        $conversionsPage = $filterByStatus($conversionsPage, $request);
268
269        /**
270         * Order and sort
271         */
272        switch ($order) {
273            // sort by campaign name
274            case 'campaign':
275                $conversionsPage->orderBy('google_ads_campaigns.campaign_name', $sort);
276                break;
277
278            // sort by customer name
279            case 'customer':
280                $conversionsPage->orderBy('google_ads_customers.customer_descriptive_name', $sort);
281                break;
282
283            // sort by click ids
284            case 'gclid':
285            case 'fbclid':
286                $conversionsPage->orderBy($order, $sort);
287                break;
288
289            case 'created_at':
290                $conversionsPage->orderBy('conversions.created_at', $sort);
291                break;
292        }
293
294        /**
295         * Page and items
296         */
297        $conversionsPage = $conversionsPage->paginate($size);
298        $conversionsItems = $conversionsPage->items();
299
300        /**
301         * Build pagination hateoas
302         */
303        $paginationHateoas = $this->createPaginationFromOrm($conversionsPage);
304
305        /**
306         * HTTP 200
307         */
308        return $this->sendResponse(200, $conversionsItems, $paginationHateoas);
309    } // getConversionsDigest
310
311
312    /**
313     * Return one conversion
314     *
315     * @author gbh
316     * @see http://swagger-humanverify.fruitbat.systems/#get-/api/conversions/-id-
317     */
318    public function getConversion(Request $request, Int $id):JsonResponse
319    {
320        $googleAdsCustomers = $this->googleAds->getGoogleAdsCustomers();
321        $googleAdsCampaignIds = self::getGoogleCampaignIds($googleAdsCustomers, $request);
322
323        /**
324         * Get conversion
325         */
326        $conversion =  Conversion::where('id', '=', $id)
327                        ->whereIn('cmpid', $googleAdsCampaignIds) // only return campaigns of the session user
328                        ->first();
329
330        /**
331         * No record
332         * HTTP 404
333         */
334        if (is_null($conversion)) {
335            return $this->sendError(404, null, null);
336        }
337
338        /**
339         * HTTP 200
340         */
341        return $this->sendResponse(200, $conversion);
342    } // getConversion
343
344
345    /**
346     * Return transactions for one conversion
347     *
348     * @author gbh
349     * @see http://swagger-humanverify.fruitbat.systems/#get-/api/conversions/-id-/transactions
350     */
351    public function getConversionTransactions(Request $request, Int $id):JsonResponse
352    {
353        $googleAdsCustomers = $this->googleAds->getGoogleAdsCustomers();
354        $googleAdsCampaignIds = self::getGoogleCampaignIds($googleAdsCustomers, $request);
355
356        /**
357         * Get conversion
358         */
359        $conversion =  Conversion::where('id', '=', $id)
360                        ->whereIn('cmpid', $googleAdsCampaignIds) // only return campaigns of the session user
361                        ->first();
362
363        /**
364         * No record
365         * HTTP 404
366         */
367        if (is_null($conversion)) {
368            return $this->sendError(404, null, null);
369        }
370
371        $transactions = ConversionTransaction::where('conversion_id', '=', $id)
372            ->orderBy('created_at', 'desc')
373            ->get();
374
375        /**
376         * HTTP 200
377         */
378        return $this->sendResponse(200, $transactions);
379    }
380
381
382    /*
383    public function postConversionApproveTest(Request $request, Int $id):JsonResponse
384    {
385         * attempting to get the 'conversion acctions' attached to a 'customer' here,
386         * see: https://groups.google.com/g/adwords-api/c/8f50BJxpaQI
387         * https://groups.google.com/g/adwords-api/c/8f50BJxpaQI
388
389        $googleCustomerIds = $this->getGoogleAdsCustomerIds();
390        $gcid = $googleCustomerIds[2];
391
392        $t = $this->getGoogleAdsCampaigns($gcid);
393        return $this->sendResponse(200, $t);
394
395
396        //$googleAdsClient = $this->getAdsClient($gcid);
397        $googleAdsClient = $this->getAdsClient();
398
399        $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient();
400        $query = 'SELECT conversion_action.id, conversion_action.name FROM conversion_action';
401        $query = 'SELECT campaign.id, campaign.name FROM campaign ORDER BY campaign.id';
402        $stream =$googleAdsServiceClient->search($gcid, $query);
403
404        foreach ($stream->iterateAllElements() as $googleAdsRow) {
405            //print_r($googleAdsRow);
406            print ".";
407            print_r($googleAdsRow->getCampaign()->getName());
408            print_r($googleAdsRow->getCampaign()->getId());
409        }
410
411
412        return $this->sendResponse(200, "END postConversionApproveTest test");
413        // end
414
415         * Get conversion
416        $conversion = Conversion::find($id);
417        print json_encode($conversion);
418
419        $googleAdsClient = $this->getAdsClient();
420
421        $conversionArgs = [
422            'name' => 'Earth to Mars Cruises Conversion #',
423            'category' => \Google\Ads\GoogleAds\V10\Enums\ConversionActionCategoryEnum\ConversionActionCategory::PBDEFAULT,
424            'type' => \Google\Ads\GoogleAds\V10\Enums\ConversionActionTypeEnum\ConversionActionType::WEBPAGE,
425            'status' => \Google\Ads\GoogleAds\V10\Enums\ConversionActionStatusEnum\ConversionActionStatus::ENABLED,
426            'view_through_lookback_window_days' => 15,
427            'value_settings' => new \Google\Ads\GoogleAds\V10\Resources\ConversionAction\ValueSettings([
428                'default_value' => 23.41,
429                'always_use_default_value' => true
430            ])
431            ];
432
433        $ca = new \Google\Ads\GoogleAds\V10\Resources\ConversionAction();
434        return $this->sendResponse(200, "postConversionApproveTest test");
435    }
436     */
437
438    /**
439     * Set on conversion as approved
440     *
441     * @note conversion_action_id comes from GET /api/googleads/customers/{id}/conversionaction
442     * @author gbh
443     * @see http://swagger-humanverify.fruitbat.systems/#post-/api/conversions/-id-/approved
444     */
445    public function postConversionApprove(Request $request, Int $id):JsonResponse
446    {
447        $googleAdsCustomers = $this->googleAds->getGoogleAdsCustomers();
448        $googleAdsCampaignIds = self::getGoogleCampaignIds($googleAdsCustomers, $request);
449
450        /**
451         * Validate request body
452         * HTTP 400
453         */
454        $validationTest = $this->__validateConversionApprovalRequest($request);
455        if (is_object($validationTest)) {
456            return $validationTest;
457        }
458
459        /**
460         * Get conversion
461         */
462        $conversion =  Conversion::where('id', '=', $id)
463                        ->whereIn('cmpid', $googleAdsCampaignIds) // only return campaigns of the session user
464                        ->first();
465
466        /**
467         * No record
468         * HTTP 404
469         */
470        if (is_null($conversion)) {
471            return $this->sendError(404, null, null);
472        }
473
474        /**
475         * Get status id for 'approved'
476         */
477        $statusId = ConversionStatus::where('name', '=', 'approved')->first()->id;
478
479        /**
480         * Get the currency code
481         */
482        $currencyCode = ConversionCurrency::where('id', '=', $conversion->conversion_currency)->first()->name;
483        
484        /**
485         * BOOKMARK
486         * Approve in google
487         */
488
489        /**
490         * Build the click conversion
491         * @see https://developers.google.com/google-ads/api/docs/samples/upload-conversion-with-identifiers
492         */
493        $campaignId = $conversion->cmpid;
494        $conversionId = $conversion->id;
495        $gclid = $conversion->gclid;
496        $currencyCode = $conversion->conversion_currency;
497        $conversionActionId = $request->conversion_action_id;
498        $conversionValue = $request->conversion_value;
499        $conversionDateTime = date("Y-m-d H:i:sP"); // store
500        $orderId = $request->order_id ?? null;
501
502        // get the customer id from the campaign id via cache
503        $customerId = $this->googleAds->getGoogleAdsCustomerFromCampaign($campaignId);
504
505        // the conversion action here is just an endpoint, but we get it the canonical way anyway.
506        $conversionAction = \Google\Ads\GoogleAds\Util\V10\ResourceNames::forConversionAction($customerId, $conversionActionId);
507
508        // the click conversion object
509        $clickConversion = new \Google\Ads\GoogleAds\V10\Services\ClickConversion([
510            'conversion_action' => $conversionAction,
511            'conversion_date_time' => $conversionDateTime,
512            'conversion_value' => $conversionValue,
513            'currency_code' => $currencyCode
514        ]);
515
516        // optional order id from body of request
517        if ($orderId) {
518            $clickConversion->setOrderId($orderId);
519        }
520
521        // BOOKMARK SET USER IDENTIFIER
522        // see https://developers.google.com/google-ads/api/docs/samples/upload-conversion-with-identifiers
523
524        /**
525         * Submit conversion to google
526         */
527        try {
528            $this->googleAds->submitConversionToGoogle($customerId, $clickConversion);
529        }
530        catch (\Exception $e) {
531            return $this->sendError(500, $e->getMessage(), null);
532        }
533
534        /**
535         * Approve locally
536         */
537
538        /**
539         * Update conversion as 'approved'
540         */
541        $conversion->status_id = $statusId;
542        $conversion->status_user_id = $this->getLoggedInUserId();
543        $conversion->approved_datetime = $conversionDateTime;
544        $conversion->save();
545
546        /**
547         * Write transaction
548         */
549        $conversionTransaction = new ConversionTransaction();
550        $conversionTransaction->conversion_id = $id;
551        $conversionTransaction->user_id = $this->getLoggedInUserId();
552        $conversionTransaction->status_id = $statusId;
553        $conversionTransaction->event = "approved";
554        $conversionTransaction->save();
555
556        /**
557         * HTTP 201
558         */
559        return $this->sendResponse(201, null);
560    } // postConversionApprove
561
562
563    /**
564     * Set one conversion as rejected
565     *
566     * @author gbh
567     * @see http://swagger-humanverify.fruitbat.systems/#post-/api/conversions/-id-/rejected
568     */
569    public function postConversionReject(Request $request, Int $id):JsonResponse
570    {
571        $googleAdsCustomers = $this->googleAds->getGoogleAdsCustomers();
572        $googleAdsCampaignIds = self::getGoogleCampaignIds($googleAdsCustomers, $request);
573
574        /**
575         * Get conversion
576         */
577        $conversion =  Conversion::where('id', '=', $id)
578                        ->whereIn('cmpid', $googleAdsCampaignIds) // only return campaigns of the session user
579                        ->first();
580
581        /**
582         * No record
583         * HTTP 404
584         */
585        if (is_null($conversion)) {
586            return $this->sendError(404, null, null);
587        }
588
589        /**
590         * Get status id for 'rejected'
591         */
592        $statusId = ConversionStatus::where('name', '=', 'rejected')->first()->id;
593
594        /**
595         * Update status id
596         */
597        $conversion->status_id = $statusId;
598        $conversion->status_user_id = $this->getLoggedInUserId();
599        $conversion->save();
600
601        /**
602         * Write transaction
603         */
604        $conversionTransaction = new ConversionTransaction();
605        $conversionTransaction->conversion_id = $id;
606        $conversionTransaction->user_id = $this->getLoggedInUserId();
607        $conversionTransaction->status_id = $statusId;
608        $conversionTransaction->event = "rejected";
609        $conversionTransaction->save();
610
611        /**
612         * HTTP 201
613         */
614        return $this->sendResponse(201, null);
615    } // postConversionReject
616
617
618    /**
619     * Set on conversion as flagged
620     *
621     * @author gbh
622     * @see http://swagger-humanverify.fruitbat.systems/#post-/api/conversions/-id-/flagged
623     */
624    public function postConversionFlag(Request $request, Int $id):JsonResponse
625    {
626        $googleAdsCustomers = $this->googleAds->getGoogleAdsCustomers();
627        $googleAdsCampaignIds = self::getGoogleCampaignIds($googleAdsCustomers, $request);
628
629        /**
630         * Get conversion
631         */
632        $conversion =  Conversion::where('id', '=', $id)
633                        ->whereIn('cmpid', $googleAdsCampaignIds) // only return campaigns of the session user
634                        ->first();
635
636        /**
637         * No record
638         * HTTP 404
639         */
640        if (is_null($conversion)) {
641            return $this->sendError(404, null, null);
642        }
643
644        /**
645         * Get status id for 'flagged'
646         */
647        $statusId = ConversionStatus::where('name', '=', 'flagged')->first()->id;
648
649        /**
650         * Update status id
651         */
652        $conversion->status_id = $statusId;
653        $conversion->status_user_id = $this->getLoggedInUserId();
654        $conversion->save();
655
656        /**
657         * Write transaction
658         */
659        $conversionTransaction = new ConversionTransaction();
660        $conversionTransaction->conversion_id = $id;
661        $conversionTransaction->user_id = $this->getLoggedInUserId();
662        $conversionTransaction->status_id = $statusId;
663        $conversionTransaction->event = "flagged";
664        $conversionTransaction->save();
665
666        /**
667         * HTTP 201
668         */
669        return $this->sendResponse(201, null);
670    } // postConversionFlag
671
672
673    /**
674     * Post one note to a conversion
675     *
676     * @author gbh
677     * @see http://swagger-humanverify.fruitbat.systems/#post-/api/conversions/-id-/note
678     */
679    public function postConversionNote(Int $id, Request $request):JsonResponse
680    {
681        $googleAdsCustomers = $this->googleAds->getGoogleAdsCustomers();
682        $googleAdsCampaignIds = self::getGoogleCampaignIds($googleAdsCustomers, $request);
683
684        /**
685         * Get conversion
686         */
687        $conversion =  Conversion::where('id', '=', $id)
688                        ->whereIn('cmpid', $googleAdsCampaignIds) // only return campaigns of the session user
689                        ->first();
690
691        /**
692         * No record
693         * HTTP 404
694         */
695        if (is_null($conversion)) {
696            return $this->sendError(404, null, null);
697        }
698
699        /**
700         * Validate request body
701         * HTTP 400
702         */
703        $validationTest = $this->__validateNoteRequest($request);
704        if (is_object($validationTest)) {
705            return $validationTest;
706        }
707
708        /**
709         * Create the note
710         */
711        $conversionNote = new ConversionNote();
712        $conversionNote->conversion_id = $id;
713        $conversionNote->note = $request->note;
714        $conversionNote->user_id = $this->getLoggedInUserId();
715        $conversionNote->save();
716
717        /**
718         * Write transaction
719         */
720        $conversionTransaction = new ConversionTransaction();
721        $conversionTransaction->conversion_id = $id;
722        $conversionTransaction->user_id = $this->getLoggedInUserId();
723        $conversionTransaction->status_id = $conversion->status_id;
724        $conversionTransaction->event = "note";
725        $conversionTransaction->note = $request->note;
726        $conversionTransaction->save();
727
728        /**
729         * HTTP 201
730         */
731        return $this->sendResponse(201, null);
732    } // postConversionNote
733
734
735    /**
736     * Validates request body of a Note
737     *
738     */
739    protected function __validateNoteRequest(Request $request) // @phpstan-ignore-line
740    {
741        $itemValidationRules = [
742            'note' => 'required|string',
743        ];
744
745        $validationResult = $this->__validateJson($itemValidationRules, $request);
746
747        if (is_array($validationResult)) {
748            return $this->sendError(400, $validationResult, "Validation error");
749        } else {
750            return true;
751        }
752    } // __validateNoteRequest
753
754
755    /**
756     * Validates request body of a Conversion
757     *
758     */
759    protected function __validateConversionRequest(Request $request) // @phpstan-ignore-line
760    {
761        $itemValidationRules = [
762            'website_url' => 'required|string',
763        ];
764
765        $validationResult = $this->__validateJson($itemValidationRules, $request);
766
767        if (is_array($validationResult)) {
768            return $this->sendError(400, $validationResult, "Validation error");
769        } else {
770            return true;
771        }
772    } // __validateConversionRequest
773
774
775    /**
776     * Validates that string $date is a valid yyyy-mm-dd date
777     */
778    protected function __validateYYYYMMDD(String $date):bool
779    {
780        if (preg_match("/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/", $date, $parts)) {
781            if (checkdate($parts[2], $parts[3], $parts[1])) {
782                return true;
783            }
784        }
785        return false;
786    }
787
788
789    /**
790     * Validates request body of conversion approval
791     *
792     */
793    protected function __validateConversionApprovalRequest(Request $request) // @phpstan-ignore-line
794    {
795        $itemValidationRules = [
796            'conversion_action_id' => 'required|integer',
797            'conversion_value' => 'required|numeric|min:0.01',
798            'order_id' => 'string',
799        ];
800
801        $validationResult = $this->__validateJson($itemValidationRules, $request);
802
803        if (is_array($validationResult)) {
804            return $this->sendError(400, $validationResult, "Validation error");
805        } else {
806            return true;
807        }
808    } // __validateNoteRequest
809
810
811    protected static function getGoogleCampaignIds(array $googleAdsCustomers, Request $request)
812    {
813        // refresh cache of customers and campaigns if required. select all customers
814        //$googleCustomers = $this->googleAds->getGoogleAdsCustomers();
815
816        // unique, non-null ids as array
817        $googleCustomerIds = array_unique(
818            array_filter(
819                array_map(
820                    fn ($n) => $n['customer_id'],
821                    $googleAdsCustomers
822                ),
823                fn ($n) => strlen($n) > 0
824            )
825        );
826           
827        // campaign cache has just been refreshed so safe to select from it
828        return GoogleAdsCampaign::whereIn('customer_id', $googleCustomerIds)->pluck('campaign_id')->toArray();
829    }
830}