Passed
Push — master ( 179526...2874e4 )
by Andreas
28:11
created

org_openpsa_sales_salesproject_deliverable_dba   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Test Coverage

Coverage 88.95%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 174
dl 0
loc 331
ccs 161
cts 181
cp 0.8895
rs 5.5199
c 6
b 0
f 0
wmc 56

17 Methods

Rating   Name   Duplication   Size   Complexity  
A _update_parent() 0 4 1
A _on_creating() 0 4 1
A _on_created() 0 3 1
A _on_updated() 0 4 2
A _on_deleted() 0 7 2
A _on_updating() 0 11 4
A invoice() 0 15 4
A run_cycle() 0 20 4
A get_state() 0 10 1
B deliver() 0 37 7
A get_at_entries() 0 5 1
A calculate_price() 0 15 4
A decline() 0 23 4
A get_cycle_identifier() 0 27 5
A end_subscription() 0 10 2
A update_units() 0 37 5
B order() 0 39 8

How to fix   Complexity   

Complex Class

Complex classes like org_openpsa_sales_salesproject_deliverable_dba often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use org_openpsa_sales_salesproject_deliverable_dba, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package org.openpsa.sales
4
 * @author Nemein Oy, http://www.nemein.com/
5
 * @copyright Nemein Oy, http://www.nemein.com/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
/**
10
 * MidCOM wrapped class for access to deliverables
11
 *
12
 * @property integer $up
13
 * @property integer $product
14
 * @property integer $supplier
15
 * @property integer $salesproject
16
 * @property string $title
17
 * @property string $description
18
 * @property float $price
19
 * @property float $invoiced
20
 * @property float $units
21
 * @property float $plannedUnits
22
 * @property float $uninvoiceableUnits
23
 * @property string $unit
24
 * @property float $pricePerUnit
25
 * @property boolean $invoiceByActualUnits
26
 * @property boolean $continuous
27
 * @property float $cost Actual cost of the delivery
28
 * @property float $plannedCost Original planned cost
29
 * @property float $costPerUnit Cost per unit, used as basis of calculations for the fields above
30
 * @property string $costType
31
 * @property integer $start Start can have two different meanings:
32
        		- for single deliveries, it's the time when delivery can start
33
        		- for subscriptions it's the subscription start
34
 * @property integer $end End can have two different meanings:
35
        		- for single deliveries, it's the delivery deadline
36
        		- for subscriptions it's the subscription end
37
 * @property integer $notify
38
 * @property integer $state State of the proposal/order
39
 * @property integer $orgOpenpsaObtype Used to a) distinguish OpenPSA objects in QB b) store object "subtype" (project vs task etc)
40
 * @package org.openpsa.sales
41
 */
42
class org_openpsa_sales_salesproject_deliverable_dba extends midcom_core_dbaobject
43
{
44
    public string $__midcom_class_name__ = __CLASS__;
45
    public string $__mgdschema_class_name__ = 'org_openpsa_salesproject_deliverable';
46
47
    const STATE_NEW = 100;
48
    const STATE_DECLINED = 300;
49
    const STATE_ORDERED = 400;
50
    const STATE_STARTED = 450;
51
    const STATE_DELIVERED = 500;
52
    const STATE_INVOICED = 600;
53
54
    private bool $_update_parent_on_save = false;
55
56 32
    public function _on_creating() : bool
57
    {
58 32
        $this->calculate_price(false);
59 32
        return true;
60
    }
61
62 32
    public function _on_created()
63
    {
64 32
        $this->_update_parent();
65
    }
66
67 18
    public function _on_updating() : bool
68
    {
69 18
        $this->calculate_price(false);
70
71 18
        if (   $this->orgOpenpsaObtype == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION
72 18
            && $this->continuous) {
73 2
            $this->end = 0;
74 17
        } elseif ($this->end < $this->start) {
75 1
            $this->end = $this->start + 1;
76
        }
77 18
        return true;
78
    }
79
80 18
    public function _on_updated()
81
    {
82 18
        if ($this->_update_parent_on_save) {
83 7
            $this->_update_parent();
84
        }
85
    }
86
87 32
    public function _on_deleted()
88
    {
89 32
        $entries = $this->get_at_entries();
90 32
        foreach ($entries as $entry) {
91 8
            $entry->delete();
92
        }
93 32
        $this->_update_parent();
94
    }
95
96 32
    private function _update_parent()
97
    {
98 32
        $project = new org_openpsa_sales_salesproject_dba($this->salesproject);
99 32
        $project->calculate_price();
100
    }
101
102 4
    public function get_state() : string
103
    {
104 4
        return match ($this->state) {
105 4
            self::STATE_NEW => 'proposed',
106 4
            self::STATE_DECLINED => 'declined',
107 4
            self::STATE_ORDERED => 'ordered',
108 4
            self::STATE_STARTED => 'started',
109 4
            self::STATE_DELIVERED => 'delivered',
110 4
            self::STATE_INVOICED => 'invoiced',
111 4
            default => ''
112 4
        };
113
    }
114
115
    /**
116
     * @return midcom_services_at_entry_dba[]
117
     */
118 32
    public function get_at_entries() : array
119
    {
120 32
        $mc = new org_openpsa_relatedto_collector($this->guid, midcom_services_at_entry_dba::class);
121 32
        $mc->add_object_constraint('method', '=', 'new_subscription_cycle');
122 32
        return $mc->get_related_objects();
123
    }
124
125 33
    public function calculate_price(bool $update = true)
126
    {
127 33
        $calculator_class = midcom_baseclasses_components_configuration::get('org.openpsa.sales', 'config')->get('calculator');
128 33
        $calculator = new $calculator_class();
129 33
        $calculator->run($this);
130 33
        $cost = $calculator->get_cost();
131 33
        $price = $calculator->get_price();
132 33
        if (   $price != $this->price
133 33
            || $cost != $this->cost) {
134 12
            $this->price = $price;
135 12
            $this->cost = $cost;
136 12
            $this->_update_parent_on_save = true;
137 12
            if ($update) {
138
                $this->update();
139
                $this->_update_parent_on_save = false;
140
            }
141
        }
142
    }
143
144
    /**
145
     * Recalculate the deliverable's unit trackers based on data from (recently updated) tasks
146
     */
147 12
    public function update_units()
148
    {
149 12
        debug_add('Units before update: ' . $this->units . ", uninvoiceable: " . $this->uninvoiceableUnits);
150
151 12
        $hours = [
152 12
            'reported' => 0,
153 12
            'invoiced' => 0,
154 12
            'invoiceable' => 0
155 12
        ];
156
157
        // List hours from tasks of the agreement
158 12
        $mc = org_openpsa_projects_task_dba::new_collector('agreement', $this->id);
159 12
        $tasks = $mc->get_rows(['reportedHours', 'invoicedHours', 'invoiceableHours']);
160
161 12
        foreach ($tasks as $task) {
162
            // Add the hours of the tasks to agreement's totals
163 12
            $hours['reported'] += $task['reportedHours'];
164 12
            $hours['invoiced'] += $task['invoicedHours'];
165 12
            $hours['invoiceable'] += $task['invoiceableHours'];
166
        }
167
168
        // Update units on the agreement with invoiceable hours
169 12
        $units = $hours['invoiceable'];
170 12
        $uninvoiceableUnits = $hours['reported'] - ($hours['invoiceable'] + $hours['invoiced']);
171
172 12
        if (   $units != $this->units
173 12
            || $uninvoiceableUnits != $this->uninvoiceableUnits) {
174 4
            debug_add("agreement values have changed, setting units to " . $units . ", uninvoiceable: " . $uninvoiceableUnits);
175 4
            $this->units = $units;
176 4
            $this->uninvoiceableUnits = $uninvoiceableUnits;
177 4
            $this->_use_rcs = false;
178
179 4
            if (!$this->update()) {
180 4
                debug_add("Agreement #{$this->id} couldn't be saved to disk, last Midgard error was: " . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
181
            }
182
        } else {
183 11
            debug_add("Agreement values are unchanged, no update necessary");
184
        }
185
    }
186
187
    /**
188
     * Manually trigger a subscription cycle run.
189
     */
190 1
    public function run_cycle() : bool
191
    {
192 1
        $at_entries = $this->get_at_entries();
193 1
        if (!isset($at_entries[0])) {
194
            debug_add('No AT entry found');
195
            return false;
196
        }
197
198 1
        $entry = $at_entries[0];
199 1
        $scheduler = new org_openpsa_invoices_scheduler($this);
200
201 1
        if (!$scheduler->run_cycle($entry->arguments['cycle'])) {
202
            debug_add('Failed to run cycle');
203
            return false;
204
        }
205 1
        if (!$entry->delete()) {
206
            debug_add('Could not delete AT entry: ' . midcom_connection::get_error_string());
207
            return false;
208
        }
209 1
        return true;
210
    }
211
212 9
    public function get_cycle_identifier(int $time) : string
213
    {
214 9
        $date = new DateTime('@' . $time, new DateTimeZone(date_default_timezone_get()));
215
216 9
        switch ($this->unit) {
217 9
            case 'm':
218
                // Monthly recurring subscription
219 5
                $identifier = $date->format('Y-m');
220 5
                break;
221 4
            case 'q':
222
                // Quarterly recurring subscription
223 1
                $identifier = ceil(((int)$date->format('n')) / 4) . 'Q' . $date->format('y');
224 1
                break;
225 3
            case 'hy':
226
                // Half-yearly recurring subscription
227 1
                $identifier = ceil(((int)$date->format('n')) / 6) . '/' . $date->format('Y');
228 1
                break;
229 2
            case 'y':
230
                // Yearly recurring subscription
231 1
                $identifier = $date->format('Y');
232 1
                break;
233
            default:
234 1
                debug_add('Unrecognized unit value "' . $this->unit . '" for deliverable ' . $this->guid, MIDCOM_LOG_INFO);
235 1
                $identifier = '';
236
        }
237
238 9
        return trim($this->title . ' ' . $identifier);
239
    }
240
241 2
    public function end_subscription() : bool
242
    {
243 2
        $this->state = self::STATE_INVOICED;
244 2
        if (!$this->update()) {
245
            return false;
246
        }
247 2
        $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
248 2
        $salesproject->mark_invoiced();
249
250 2
        return true;
251
    }
252
253 1
    public function invoice() : bool
254
    {
255 1
        if (   $this->state >= self::STATE_INVOICED
256 1
            || $this->orgOpenpsaObtype == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION) {
257
            return false;
258
        }
259
260 1
        $calculator = new org_openpsa_invoices_calculator();
261 1
        $amount = $calculator->process_deliverable($this);
262
263 1
        if ($amount > 0) {
264 1
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
265 1
            $salesproject->mark_invoiced();
266
        }
267 1
        return true;
268
    }
269
270 1
    public function decline() : bool
271
    {
272 1
        if ($this->state >= self::STATE_DECLINED) {
273 1
            return false;
274
        }
275
276 1
        $this->state = self::STATE_DECLINED;
277
278 1
        if ($this->update()) {
279
            // Update sales project if it doesn't have any open deliverables
280 1
            $qb = self::new_query_builder();
281 1
            $qb->add_constraint('salesproject', '=', $this->salesproject);
282 1
            $qb->add_constraint('state', '<>', self::STATE_DECLINED);
283 1
            if ($qb->count() == 0) {
284
                // No proposals that are not declined
285 1
                $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
286 1
                $salesproject->state = org_openpsa_sales_salesproject_dba::STATE_LOST;
287 1
                $salesproject->update();
288
            }
289
290 1
            return true;
291
        }
292
        return false;
293
    }
294
295 5
    public function order() : bool
296
    {
297 5
        if ($this->state >= self::STATE_ORDERED) {
298 1
            return false;
299
        }
300
301 4
        if ($this->invoiceByActualUnits) {
302 1
            $this->cost = 0;
303 1
            $this->units = 0;
304
        }
305
306
        // Check what kind of order this is
307 4
        $product = org_openpsa_products_product_dba::get_cached($this->product);
308 4
        $scheduler = new org_openpsa_invoices_scheduler($this);
309
310 4
        if ($product->delivery == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION) {
0 ignored issues
show
Bug Best Practice introduced by
The property delivery does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
311
            // This is a new subscription, initiate the cycle but don't send invoice
312 1
            if (!$scheduler->run_cycle(1, false)) {
313 1
                return false;
314
            }
315 3
        } elseif ($product->orgOpenpsaObtype === org_openpsa_products_product_dba::TYPE_SERVICE) {
0 ignored issues
show
Bug Best Practice introduced by
The property orgOpenpsaObtype does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
316
            $scheduler->create_task($this->start, $this->end, $this->title);
317
        }
318
        // TODO: Warehouse management: create new order (for org_openpsa_products_product_dba::TYPE_GOODS)
319
320 4
        $this->state = self::STATE_ORDERED;
321
322 4
        if ($this->update()) {
323
            // Update sales project and mark as won
324 4
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
325 4
            if ($salesproject->state != org_openpsa_sales_salesproject_dba::STATE_WON) {
326 2
                $salesproject->state = org_openpsa_sales_salesproject_dba::STATE_WON;
327 2
                $salesproject->update();
328
            }
329
330 4
            return true;
331
        }
332
333
        return false;
334
    }
335
336 3
    public function deliver(bool $update_deliveries = true) : bool
337
    {
338 3
        if ($this->state > self::STATE_DELIVERED) {
339
            return false;
340
        }
341
342 3
        $product = org_openpsa_products_product_dba::get_cached($this->product);
343 3
        if ($product->delivery == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION) {
0 ignored issues
show
Bug Best Practice introduced by
The property delivery does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
344
            // Subscriptions are ongoing, not one delivery
345 2
            return false;
346
        }
347
348 1
        $this->state = self::STATE_DELIVERED;
349 1
        $this->end = time();
350 1
        if ($this->update()) {
351
            // Update sales project and mark as delivered (if no other deliverables are active)
352 1
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
353 1
            $salesproject->mark_delivered();
354
355 1
            midcom::get()->uimessages->add(midcom::get()->i18n->get_string('org.openpsa.sales', 'org.openpsa.sales'), sprintf(midcom::get()->i18n->get_string('marked deliverable "%s" delivered', 'org.openpsa.sales'), $this->title));
356
357
            // Check if we need to create task or ship goods
358 1
            if (   $update_deliveries
359 1
                && $product->orgOpenpsaObtype === org_openpsa_products_product_dba::TYPE_SERVICE) {
0 ignored issues
show
Bug Best Practice introduced by
The property orgOpenpsaObtype does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
360
                // Change status of tasks connected to the deliverable
361
                $qb = org_openpsa_projects_task_dba::new_query_builder();
362
                $qb->add_constraint('agreement', '=', $this->id);
363
                $qb->add_constraint('status', '<', org_openpsa_projects_task_status_dba::CLOSED);
364
                foreach ($qb->execute() as $task) {
365
                    org_openpsa_projects_workflow::close($task, sprintf(midcom::get()->i18n->get_string('completed from deliverable %s', 'org.openpsa.sales'), $this->title));
366
                }
367
                // TODO: Warehouse management: mark product as shipped (for org_openpsa_products_product_dba::TYPE_GOODS)
368
            }
369
370 1
            return true;
371
        }
372
        return false;
373
    }
374
}
375