Passed
Push — master ( 49a209...f5c71d )
by Andreas
24:42
created

org_openpsa_sales_salesproject_deliverable_dba   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Test Coverage

Coverage 87.91%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 180
dl 0
loc 340
ccs 160
cts 182
cp 0.8791
rs 3.44
c 5
b 0
f 0
wmc 62

17 Methods

Rating   Name   Duplication   Size   Complexity  
A _update_parent() 0 4 1
A run_cycle() 0 20 4
B get_state() 0 17 7
A _on_creating() 0 4 1
A get_at_entries() 0 5 1
A calculate_price() 0 15 4
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 update_units() 0 37 5
A invoice() 0 15 4
B deliver() 0 37 7
A decline() 0 23 4
A get_cycle_identifier() 0 27 5
A end_subscription() 0 10 2
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 $__midcom_class_name__ = __CLASS__;
45
    public $__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
    /**
55
     * @var boolean
56
     */
57
    private $_update_parent_on_save = false;
58
59 32
    public function _on_creating() : bool
60
    {
61 32
        $this->calculate_price(false);
62 32
        return true;
63
    }
64
65 32
    public function _on_created()
66
    {
67 32
        $this->_update_parent();
68 32
    }
69
70 18
    public function _on_updating() : bool
71
    {
72 18
        $this->calculate_price(false);
73
74 18
        if (   $this->orgOpenpsaObtype == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION
75 18
            && $this->continuous) {
76 2
            $this->end = 0;
77 17
        } elseif ($this->end < $this->start) {
78 1
            $this->end = $this->start + 1;
79
        }
80 18
        return true;
81
    }
82
83 18
    public function _on_updated()
84
    {
85 18
        if ($this->_update_parent_on_save) {
86 7
            $this->_update_parent();
87
        }
88 18
    }
89
90 32
    public function _on_deleted()
91
    {
92 32
        $entries = $this->get_at_entries();
93 32
        foreach ($entries as $entry) {
94 8
            $entry->delete();
95
        }
96 32
        $this->_update_parent();
97 32
    }
98
99 32
    private function _update_parent()
100
    {
101 32
        $project = new org_openpsa_sales_salesproject_dba($this->salesproject);
102 32
        $project->calculate_price();
103 32
    }
104
105 4
    public function get_state() : string
106
    {
107 4
        switch ($this->state) {
108
            case self::STATE_NEW:
109
                return 'proposed';
110
            case self::STATE_DECLINED:
111
                return 'declined';
112
            case self::STATE_ORDERED:
113 3
                return 'ordered';
114
            case self::STATE_STARTED:
115 1
                return 'started';
116
            case self::STATE_DELIVERED:
117 1
                return 'delivered';
118
            case self::STATE_INVOICED:
119 1
                return 'invoiced';
120
        }
121 3
        return '';
122
    }
123
124
    /**
125
     * @return midcom_services_at_entry_dba[]
126
     */
127 32
    public function get_at_entries() : array
128
    {
129 32
        $mc = new org_openpsa_relatedto_collector($this->guid, midcom_services_at_entry_dba::class);
130 32
        $mc->add_object_constraint('method', '=', 'new_subscription_cycle');
131 32
        return $mc->get_related_objects();
132
    }
133
134 33
    public function calculate_price(bool $update = true)
135
    {
136 33
        $calculator_class = midcom_baseclasses_components_configuration::get('org.openpsa.sales', 'config')->get('calculator');
137 33
        $calculator = new $calculator_class();
138 33
        $calculator->run($this);
139 33
        $cost = $calculator->get_cost();
140 33
        $price = $calculator->get_price();
141 33
        if (   $price != $this->price
142 33
            || $cost != $this->cost) {
143 12
            $this->price = $price;
144 12
            $this->cost = $cost;
145 12
            $this->_update_parent_on_save = true;
146 12
            if ($update) {
147
                $this->update();
148
                $this->_update_parent_on_save = false;
149
            }
150
        }
151 33
    }
152
153
    /**
154
     * Recalculate the deliverable's unit trackers based on data form a (recently updated) task
155
     */
156 12
    public function update_units()
157
    {
158 12
        debug_add('Units before update: ' . $this->units . ", uninvoiceable: " . $this->uninvoiceableUnits);
159
160
        $hours = [
161 12
            'reported' => 0,
162
            'invoiced' => 0,
163
            'invoiceable' => 0
164
        ];
165
166
        // List hours from tasks of the agreement
167 12
        $mc = org_openpsa_projects_task_dba::new_collector('agreement', $this->id);
168 12
        $other_tasks = $mc->get_rows(['reportedHours', 'invoicedHours', 'invoiceableHours']);
169
170 12
        foreach ($other_tasks as $other_task) {
171
            // Add the hours of the other tasks to agreement's totals
172 12
            $hours['reported'] += $other_task['reportedHours'];
173 12
            $hours['invoiced'] += $other_task['invoicedHours'];
174 12
            $hours['invoiceable'] += $other_task['invoiceableHours'];
175
        }
176
177
        // Update units on the agreement with invoiceable hours
178 12
        $units = $hours['invoiceable'];
179 12
        $uninvoiceableUnits = $hours['reported'] - ($hours['invoiceable'] + $hours['invoiced']);
180
181 12
        if (   $units != $this->units
182 12
            || $uninvoiceableUnits != $this->uninvoiceableUnits) {
183 4
            debug_add("agreement values have changed, setting units to " . $units . ", uninvoiceable: " . $uninvoiceableUnits);
184 4
            $this->units = $units;
185 4
            $this->uninvoiceableUnits = $uninvoiceableUnits;
186 4
            $this->_use_rcs = false;
187
188 4
            if (!$this->update()) {
189 4
                debug_add("Agreement #{$this->id} couldn't be saved to disk, last Midgard error was: " . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
190
            }
191
        } else {
192 11
            debug_add("Agreement values are unchanged, no update necessary");
193
        }
194 12
    }
195
196
    /**
197
     * Manually trigger a subscription cycle run.
198
     */
199 1
    public function run_cycle() : bool
200
    {
201 1
        $at_entries = $this->get_at_entries();
202 1
        if (!isset($at_entries[0])) {
203
            debug_add('No AT entry found');
204
            return false;
205
        }
206
207 1
        $entry = $at_entries[0];
208 1
        $scheduler = new org_openpsa_invoices_scheduler($this);
209
210 1
        if (!$scheduler->run_cycle($entry->arguments['cycle'])) {
211
            debug_add('Failed to run cycle');
212
            return false;
213
        }
214 1
        if (!$entry->delete()) {
215
            debug_add('Could not delete AT entry: ' . midcom_connection::get_error_string());
216
            return false;
217
        }
218 1
        return true;
219
    }
220
221 9
    public function get_cycle_identifier(int $time) : string
222
    {
223 9
        $date = new DateTime(gmdate('Y-m-d', $time), new DateTimeZone('GMT'));
224
225 9
        switch ($this->unit) {
226 9
            case 'm':
227
                // Monthly recurring subscription
228 5
                $identifier = $date->format('Y-m');
229 5
                break;
230 4
            case 'q':
231
                // Quarterly recurring subscription
232 1
                $identifier = ceil(((int)$date->format('n')) / 4) . 'Q' . $date->format('y');
233 1
                break;
234 3
            case 'hy':
235
                // Half-yearly recurring subscription
236 1
                $identifier = ceil(((int)$date->format('n')) / 6) . '/' . $date->format('Y');
237 1
                break;
238 2
            case 'y':
239
                // Yearly recurring subscription
240 1
                $identifier = $date->format('Y');
241 1
                break;
242
            default:
243 1
                debug_add('Unrecognized unit value "' . $this->unit . '" for deliverable ' . $this->guid, MIDCOM_LOG_INFO);
244 1
                $identifier = '';
245
        }
246
247 9
        return trim($this->title . ' ' . $identifier);
248
    }
249
250 2
    public function end_subscription() : bool
251
    {
252 2
        $this->state = self::STATE_INVOICED;
253 2
        if (!$this->update()) {
254
            return false;
255
        }
256 2
        $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
257 2
        $salesproject->mark_invoiced();
258
259 2
        return true;
260
    }
261
262 1
    public function invoice() : bool
263
    {
264 1
        if (   $this->state >= self::STATE_INVOICED
265 1
            || $this->orgOpenpsaObtype == org_openpsa_products_product_dba::DELIVERY_SUBSCRIPTION) {
266
            return false;
267
        }
268
269 1
        $calculator = new org_openpsa_invoices_calculator();
270 1
        $amount = $calculator->process_deliverable($this);
271
272 1
        if ($amount > 0) {
273 1
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
274 1
            $salesproject->mark_invoiced();
275
        }
276 1
        return true;
277
    }
278
279 1
    public function decline() : bool
280
    {
281 1
        if ($this->state >= self::STATE_DECLINED) {
282 1
            return false;
283
        }
284
285 1
        $this->state = self::STATE_DECLINED;
286
287 1
        if ($this->update()) {
288
            // Update sales project if it doesn't have any open deliverables
289 1
            $qb = self::new_query_builder();
290 1
            $qb->add_constraint('salesproject', '=', $this->salesproject);
291 1
            $qb->add_constraint('state', '<>', self::STATE_DECLINED);
292 1
            if ($qb->count() == 0) {
293
                // No proposals that are not declined
294 1
                $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
295 1
                $salesproject->state = org_openpsa_sales_salesproject_dba::STATE_LOST;
296 1
                $salesproject->update();
297
            }
298
299 1
            return true;
300
        }
301
        return false;
302
    }
303
304 5
    public function order() : bool
305
    {
306 5
        if ($this->state >= self::STATE_ORDERED) {
307 1
            return false;
308
        }
309
310 4
        if ($this->invoiceByActualUnits) {
311 1
            $this->cost = 0;
312 1
            $this->units = 0;
313
        }
314
315
        // Check what kind of order this is
316 4
        $product = org_openpsa_products_product_dba::get_cached($this->product);
317 4
        $scheduler = new org_openpsa_invoices_scheduler($this);
318
319 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...
320
            // This is a new subscription, initiate the cycle but don't send invoice
321 1
            if (!$scheduler->run_cycle(1, false)) {
322 1
                return false;
323
            }
324 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...
325
            $scheduler->create_task($this->start, $this->end, $this->title);
326
        }
327
        // TODO: Warehouse management: create new order (for org_openpsa_products_product_dba::TYPE_GOODS)
328
329 4
        $this->state = self::STATE_ORDERED;
330
331 4
        if ($this->update()) {
332
            // Update sales project and mark as won
333 4
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
334 4
            if ($salesproject->state != org_openpsa_sales_salesproject_dba::STATE_WON) {
335 2
                $salesproject->state = org_openpsa_sales_salesproject_dba::STATE_WON;
336 2
                $salesproject->update();
337
            }
338
339 4
            return true;
340
        }
341
342
        return false;
343
    }
344
345 3
    public function deliver(bool $update_deliveries = true) : bool
346
    {
347 3
        if ($this->state > self::STATE_DELIVERED) {
348
            return false;
349
        }
350
351 3
        $product = org_openpsa_products_product_dba::get_cached($this->product);
352 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...
353
            // Subscriptions are ongoing, not one delivery
354 2
            return false;
355
        }
356
357 1
        $this->state = self::STATE_DELIVERED;
358 1
        $this->end = time();
359 1
        if ($this->update()) {
360
            // Update sales project and mark as delivered (if no other deliverables are active)
361 1
            $salesproject = new org_openpsa_sales_salesproject_dba($this->salesproject);
362 1
            $salesproject->mark_delivered();
363
364 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));
365
366
            // Check if we need to create task or ship goods
367 1
            if (   $update_deliveries
368 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...
369
                // Change status of tasks connected to the deliverable
370
                $qb = org_openpsa_projects_task_dba::new_query_builder();
371
                $qb->add_constraint('agreement', '=', $this->id);
372
                $qb->add_constraint('status', '<', org_openpsa_projects_task_status_dba::CLOSED);
373
                foreach ($qb->execute() as $task) {
374
                    org_openpsa_projects_workflow::close($task, sprintf(midcom::get()->i18n->get_string('completed from deliverable %s', 'org.openpsa.sales'), $this->title));
375
                }
376
                // TODO: Warehouse management: mark product as shipped (for org_openpsa_products_product_dba::TYPE_GOODS)
377
            }
378
379 1
            return true;
380
        }
381
        return false;
382
    }
383
}
384