Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 235
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
QuickBooksController
0.00% covered (danger)
0.00%
0 / 235
0.00% covered (danger)
0.00%
0 / 15
2756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 jsonResponse
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 displaySetup
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
20
 getStatus
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 disconnect
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getAccountMappings
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 updateAccountMapping
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 updateAccountMappingsBatch
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
72
 getChartOfAccounts
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 getSyncHistory
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 manualSync
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 receiveDailyData
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 getDailyCloseData
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 calculateDailyTotals
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mapDRSIdToQBAccountId
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace BuyerKiosk\QuickBooks\Controllers;
4
5
6use BuyerKiosk\QuickBooks\JournalEntryService;
7use BuyerKiosk\QuickBooks\QuickBooksService;
8/**
9 * QuickBooks Controller
10 *
11 * Handles all QuickBooks integration endpoints including OAuth setup,
12 * account mapping, and data sync operations.
13 */
14class QuickBooksController extends \BuyerKiosk\Core\Controllers\BaseController
15{
16    private $store;
17    private $app;
18    private $qbService;
19
20    public function __construct($app, \Store $store)
21    {
22        $this->store = $store;
23        $this->app = $app;
24        $this->qbService = new QuickBooksService($store);
25    }
26
27    /**
28     * Output JSON response with proper content type
29     */
30    private function jsonResponse($data)
31    {
32        $this->app->response->headers->set('Content-Type', 'application/json');
33        echo json_encode($data);
34    }
35
36    /**
37     * Display the QuickBooks setup/configuration page
38     */
39    public function displaySetup()
40    {
41        // Check if OAuth credentials are configured
42        if (!$this->qbService->isConfigured()) {
43            $this->app->render('qbconnect/setup.html', [
44                'page' => [
45                    'author' => $this->app->site->author,
46                    'title' => "QuickBooks Connect | Setup",
47                    'description' => "Connect your QuickBooks Online account",
48                    'alerts' => $this->app->alerts->getAndClearMessages(),
49                ],
50                'groups' => $this->app->user->getGroups(),
51                'box_title' => "QuickBooks Connect Setup",
52                'store' => getStoreInfo($this->store),
53                'configError' => true,
54                'errorMessage' => 'QuickBooks integration is not configured. Please contact support.'
55            ]);
56            return;
57        }
58
59        $authUrl = $this->qbService->getAuthorizationUrl();
60        $isConnected = $this->store->isQbConfigured();
61        $companyInfo = null;
62        $accounts = [];
63        $accountMappings = [];
64
65        if ($isConnected) {
66            // Get company info and accounts
67            $companyInfo = $this->qbService->getCompanyInfo();
68            $accounts = $this->qbService->getAccounts();
69            $accountMappings = $this->qbService->getAccountMappings();
70
71            // Convert mappings to keyed array for easy lookup
72            $mappingsById = [];
73            foreach ($accountMappings as $mapping) {
74                $mappingsById[$mapping['fieldName']] = $mapping;
75            }
76            $accountMappings = $mappingsById;
77        }
78
79        $this->app->render('qbconnect/setup.html', [
80            'page' => [
81                'author' => $this->app->site->author,
82                'title' => "QuickBooks Connect | Setup",
83                'description' => "Connect your QuickBooks Online account",
84                'alerts' => $this->app->alerts->getAndClearMessages(),
85            ],
86            'groups' => $this->app->user->getGroups(),
87            'box_title' => "QuickBooks Connect Setup",
88            'store' => getStoreInfo($this->store),
89            'authUrl' => $authUrl,
90            'isConnected' => $isConnected,
91            'companyInfo' => $companyInfo,
92            'accounts' => $accounts,
93            'accountMappings' => $accountMappings,
94            'lastSync' => $this->store->getQbLastSync(),
95            'appDomain' => QuickBooksService::getAppDomain()
96        ]);
97    }
98
99    /**
100     * API: Get connection status
101     */
102    public function getStatus()
103    {
104        $response = [
105            'connected' => $this->store->isQbConfigured(),
106            'enabled' => (bool)$this->store->getQbEnabled(),
107            'lastSync' => $this->store->getQbLastSync(),
108            'realmId' => $this->store->getQbRealmID()
109        ];
110
111        if ($response['connected']) {
112            $companyInfo = $this->qbService->getCompanyInfo();
113            if ($companyInfo) {
114                $response['companyName'] = $companyInfo->CompanyName ?? null;
115            }
116        }
117
118        $this->jsonResponse($response);
119    }
120
121    /**
122     * API: Disconnect QuickBooks
123     */
124    public function disconnect()
125    {
126        $this->qbService->disconnect();
127
128        $this->jsonResponse([
129            'success' => true,
130            'message' => 'QuickBooks has been disconnected'
131        ]);
132    }
133
134    /**
135     * API: Get all account mappings
136     */
137    public function getAccountMappings()
138    {
139        $mappings = $this->qbService->getAccountMappings();
140
141        // Group by category for easier UI display
142        $grouped = [];
143        foreach ($mappings as $mapping) {
144            $category = $mapping['fieldCategory'] ?? 'other';
145            if (!isset($grouped[$category])) {
146                $grouped[$category] = [];
147            }
148            $grouped[$category][] = $mapping;
149        }
150
151        $this->jsonResponse([
152            'success' => true,
153            'mappings' => $mappings,
154            'grouped' => $grouped
155        ]);
156    }
157
158    /**
159     * API: Update a single account mapping
160     */
161    public function updateAccountMapping()
162    {
163        $fieldName = $this->app->request->post('fieldName');
164        $qbAccountId = $this->app->request->post('qbAccountId');
165        $qbAccountName = $this->app->request->post('qbAccountName');
166        $qbAccountType = $this->app->request->post('qbAccountType');
167
168        if (empty($fieldName)) {
169            $this->app->response->setStatus(400);
170            $this->jsonResponse(['success' => false, 'error' => 'Field name is required']);
171            return;
172        }
173
174        // Allow clearing the mapping by setting to null/empty
175        if (empty($qbAccountId) || $qbAccountId === '0' || $qbAccountId === 'none') {
176            $qbAccountId = null;
177            $qbAccountName = null;
178            $qbAccountType = null;
179        }
180
181        $success = $this->qbService->updateAccountMapping(
182            $fieldName,
183            $qbAccountId,
184            $qbAccountName,
185            $qbAccountType
186        );
187
188        if ($success) {
189            $this->jsonResponse([
190                'success' => true,
191                'message' => 'Mapping updated successfully'
192            ]);
193        } else {
194            $this->app->response->setStatus(500);
195            $this->jsonResponse([
196                'success' => false,
197                'error' => 'Failed to update mapping'
198            ]);
199        }
200    }
201
202    /**
203     * API: Batch update multiple account mappings
204     */
205    public function updateAccountMappingsBatch()
206    {
207        $mappingsJson = $this->app->request->post('mappings');
208        $mappings = json_decode($mappingsJson, true);
209
210        if (!is_array($mappings)) {
211            $this->app->response->setStatus(400);
212            $this->jsonResponse(['success' => false, 'error' => 'Invalid mappings data']);
213            return;
214        }
215
216        $updated = 0;
217        $errors = [];
218
219        foreach ($mappings as $mapping) {
220            if (empty($mapping['fieldName'])) {
221                continue;
222            }
223
224            $qbAccountId = $mapping['qbAccountId'] ?? null;
225            $qbAccountName = $mapping['qbAccountName'] ?? null;
226            $qbAccountType = $mapping['qbAccountType'] ?? null;
227
228            if (empty($qbAccountId) || $qbAccountId === '0' || $qbAccountId === 'none') {
229                $qbAccountId = null;
230                $qbAccountName = null;
231                $qbAccountType = null;
232            }
233
234            if ($this->qbService->updateAccountMapping(
235                $mapping['fieldName'],
236                $qbAccountId,
237                $qbAccountName,
238                $qbAccountType
239            )) {
240                $updated++;
241            } else {
242                $errors[] = $mapping['fieldName'];
243            }
244        }
245
246        $this->jsonResponse([
247            'success' => count($errors) === 0,
248            'updated' => $updated,
249            'errors' => $errors
250        ]);
251    }
252
253    /**
254     * API: Get QuickBooks Chart of Accounts
255     */
256    public function getChartOfAccounts()
257    {
258        if (!$this->store->isQbConfigured()) {
259            $this->app->response->setStatus(401);
260            $this->jsonResponse(['success' => false, 'error' => 'QuickBooks not connected']);
261            return;
262        }
263
264        $accounts = $this->qbService->getAccounts();
265
266        // Format accounts for dropdown display
267        $formattedAccounts = [];
268        foreach ($accounts as $account) {
269            $formattedAccounts[] = [
270                'id' => $account->Id,
271                'name' => $account->Name,
272                'fullName' => $account->FullyQualifiedName ?? $account->Name,
273                'type' => $account->AccountType,
274                'subType' => $account->AccountSubType ?? null,
275                'number' => $account->AcctNum ?? null,
276                'isSubAccount' => $account->SubAccount === 'true',
277                'active' => $account->Active === 'true'
278            ];
279        }
280
281        // Sort by account number then name
282        usort($formattedAccounts, function($a, $b) {
283            if ($a['number'] && $b['number']) {
284                return strcmp($a['number'], $b['number']);
285            }
286            if ($a['number']) return -1;
287            if ($b['number']) return 1;
288            return strcmp($a['name'], $b['name']);
289        });
290
291        $this->jsonResponse([
292            'success' => true,
293            'accounts' => $formattedAccounts
294        ]);
295    }
296
297    /**
298     * API: Get sync history
299     */
300    public function getSyncHistory()
301    {
302        $limit = (int)($this->app->request->get('limit') ?? 30);
303        $limit = min($limit, 100);
304
305        $db = dbConnectByName($this->store->getDbName());
306        $stmt = $db->prepare("
307            SELECT * FROM qb_sync_log
308            ORDER BY createdAt DESC
309            LIMIT :limit
310        ");
311        $stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
312        $stmt->execute();
313        $history = $stmt->fetchAll(\PDO::FETCH_ASSOC);
314
315        $this->jsonResponse([
316            'success' => true,
317            'history' => $history
318        ]);
319    }
320
321    /**
322     * API: Manual sync for a specific date
323     */
324    public function manualSync()
325    {
326        if (!$this->store->isQbConfigured()) {
327            $this->app->response->setStatus(401);
328            $this->jsonResponse(['success' => false, 'error' => 'QuickBooks not connected']);
329            return;
330        }
331
332        $date = $this->app->request->post('date');
333        if (empty($date)) {
334            $this->app->response->setStatus(400);
335            $this->jsonResponse(['success' => false, 'error' => 'Date is required']);
336            return;
337        }
338
339        // Validate date format
340        $dateObj = \DateTime::createFromFormat('Y-m-d', $date);
341        if (!$dateObj || $dateObj->format('Y-m-d') !== $date) {
342            $this->app->response->setStatus(400);
343            $this->jsonResponse(['success' => false, 'error' => 'Invalid date format. Use YYYY-MM-DD']);
344            return;
345        }
346
347        // Get daily close data for this date
348        $dailyData = $this->getDailyCloseData($date);
349        if (empty($dailyData)) {
350            $this->jsonResponse(['success' => false, 'error' => 'No close data found for this date']);
351            return;
352        }
353
354        // Create journal entry
355        $journalService = new JournalEntryService($this->store, $this->qbService);
356        $userId = $this->app->user->id ?? null;
357        $result = $journalService->syncDailyClose($date, $dailyData, 'manual', $userId);
358
359        $this->jsonResponse($result);
360    }
361
362    /**
363     * API: Receive daily close data and sync to QuickBooks
364     * This is called by the POS system or close process
365     */
366    public function receiveDailyData()
367    {
368        if (!$this->store->isQbConfigured()) {
369            $this->app->response->setStatus(401);
370            $this->jsonResponse(['success' => false, 'error' => 'QuickBooks not connected']);
371            return;
372        }
373
374        $date = $this->app->request->post('date');
375        $dataJson = $this->app->request->post('data');
376
377        if (empty($date) || empty($dataJson)) {
378            $this->app->response->setStatus(400);
379            $this->jsonResponse(['success' => false, 'error' => 'Date and data are required']);
380            return;
381        }
382
383        $dailyData = json_decode($dataJson, true);
384        if (!is_array($dailyData)) {
385            $this->app->response->setStatus(400);
386            $this->jsonResponse(['success' => false, 'error' => 'Invalid data format']);
387            return;
388        }
389
390        // Create journal entry
391        $journalService = new JournalEntryService($this->store, $this->qbService);
392        $result = $journalService->syncDailyClose($date, $dailyData, 'daily_close', null);
393
394        $this->jsonResponse($result);
395    }
396
397    /**
398     * Get daily close data for a specific date
399     *
400     * @param string $date
401     * @return array
402     */
403    private function getDailyCloseData(string $date)
404    {
405        $db = dbConnectByName($this->store->getDbName());
406
407        // Try to get from drsDailySFileData table first (if using DRS integration)
408        try {
409            $stmt = $db->prepare("SELECT * FROM drsDailySFileData WHERE closeDate = :date LIMIT 1");
410            $stmt->execute([':date' => $date]);
411            $row = $stmt->fetch(\PDO::FETCH_ASSOC);
412
413            if ($row) {
414                // Remove non-financial fields
415                unset($row['id'], $row['closeDate'], $row['createdAt'], $row['updatedAt']);
416                return $row;
417            }
418        } catch (\Exception $e) {
419            // Table may not exist
420        }
421
422        // Otherwise, calculate from transaction data if available
423        return $this->calculateDailyTotals($date);
424    }
425
426    /**
427     * Calculate daily totals from transaction data
428     *
429     * @param string $date
430     * @return array
431     */
432    private function calculateDailyTotals(string $date)
433    {
434        // This would need to be customized based on your actual data structure
435        // For now, return empty array to indicate manual entry needed
436        return [];
437    }
438
439    /**
440     * Legacy method for backward compatibility
441     * @deprecated Use updateAccountMapping instead
442     */
443    public function mapDRSIdToQBAccountId($drsID, $qbID)
444    {
445        // Get the field name from the old drsFields table if it exists
446        $db = dbConnectByName($this->store->getDbName());
447        $stmt = $db->prepare("SELECT sfileField FROM drsFields WHERE id = :id");
448        $stmt->execute([':id' => $drsID]);
449        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
450
451        if ($row) {
452            return $this->qbService->updateAccountMapping($row['sfileField'], $qbID, null, null);
453        }
454
455        return false;
456    }
457}