Passed
Push — master ( 3b4316...c86071 )
by Andreas
12:30
created

org_openpsa_invoices_scheduler   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 357
Duplicated Lines 0 %

Test Coverage

Coverage 93.97%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 46
eloc 196
c 2
b 0
f 0
dl 0
loc 357
ccs 187
cts 199
cp 0.9397
rs 8.72

10 Methods

Rating   Name   Duplication   Size   Complexity  
A get_cycle_start() 0 13 3
A _create_at_entry() 0 19 2
A create_task() 0 44 4
B _notify_owner() 0 58 5
C run_cycle() 0 75 13
A render_task_info() 0 11 3
A __construct() 0 5 1
B calculate_cycle_next() 0 35 7
A calculate_cycles() 0 28 6
A _add_month() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like org_openpsa_invoices_scheduler 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_invoices_scheduler, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package org.openpsa.invoices
4
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
/**
10
 * Helper class to process subscription invoicing
11
 *
12
 * @package org.openpsa.invoices
13
 */
14
class org_openpsa_invoices_scheduler
15
{
16
    use midcom_baseclasses_components_base;
0 ignored issues
show
introduced by
The trait midcom_baseclasses_components_base requires some properties which are not provided by org_openpsa_invoices_scheduler: $i18n, $head
Loading history...
17
18
    private org_openpsa_sales_salesproject_deliverable_dba $_deliverable;
19
20
    /**
21
     * The day of month on which subscriptions are invoiced (if none is set, they are invoiced continuously)
22
     *
23
     * @var int
24
     */
25
    private $subscription_day;
26
27 28
    public function __construct(org_openpsa_sales_salesproject_deliverable_dba $deliverable)
28
    {
29 28
        $this->_component = 'org.openpsa.invoices';
30 28
        $this->_deliverable = $deliverable;
31 28
        $this->subscription_day = midcom_baseclasses_components_configuration::get('org.openpsa.sales', 'config')->get('subscription_invoice_day_of_month');
32
    }
33
34
    /**
35
     * Initiates a new subscription cycle and registers a midcom.services.at call for the next cycle.
36
     *
37
     * The subscription cycles rely on midcom.services.at. I'm not sure if it is wise to rely on it for such
38
     * a totally mission critical part of OpenPSA. Some safeguards might be wise to add.
39
     */
40 8
    public function run_cycle(int $cycle_number, bool $send_invoice = true) : bool
41
    {
42 8
        if (time() < $this->_deliverable->start) {
43 1
            debug_add('Subscription hasn\'t started yet, register the start-up event to $start');
44 1
            return $this->_create_at_entry($cycle_number, $this->_deliverable->start);
45
        }
46
47 7
        debug_add('Running cycle ' . $cycle_number . ' for deliverable "' . $this->_deliverable->title . '"');
48
49 7
        $this_cycle_start = $this->get_cycle_start($cycle_number, time());
50 7
        if ($this->subscription_day && $cycle_number == 1) {
51
            // If there's a fixed day for invoicing, get_cycle_start already picked a future date for cycle 1
52
            $next_cycle_start = $this_cycle_start + 2; // +2 so we don't get overlaps in task
53
        } else {
54 7
            $next_cycle_start = $this->calculate_cycle_next($this_cycle_start);
55
        }
56 7
        $product = org_openpsa_products_product_dba::get_cached($this->_deliverable->product);
57
58 7
        if ($this->_deliverable->state < org_openpsa_sales_salesproject_deliverable_dba::STATE_STARTED) {
59 2
            $this->_deliverable->state = org_openpsa_sales_salesproject_deliverable_dba::STATE_STARTED;
60 2
            $this->_deliverable->update();
61
        }
62
63 7
        if ($send_invoice) {
64 7
            $calculator = new org_openpsa_invoices_calculator();
65 7
            $this_cycle_amount = $calculator->process_deliverable($this->_deliverable, $cycle_number);
66
        }
67
68 7
        $tasks_completed = [];
69 7
        $tasks_not_completed = [];
70 7
        $new_task = null;
71
72 7
        if ($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...
73
            // Close previous task(s)
74 2
            $last_task = null;
75
76 2
            $qb = org_openpsa_projects_task_dba::new_query_builder();
77 2
            $qb->add_constraint('agreement', '=', $this->_deliverable->id);
78 2
            $qb->add_constraint('status', '<', org_openpsa_projects_task_status_dba::COMPLETED);
79
80 2
            foreach ($qb->execute() as $task) {
81 2
                if (org_openpsa_projects_workflow::complete($task, sprintf($this->_i18n->get_string('completed by subscription %s', 'org.openpsa.sales'), $cycle_number))) {
82 2
                    $tasks_completed[] = $task;
83
                } else {
84
                    $tasks_not_completed[] = $task;
85
                }
86 2
                $last_task = $task;
87
            }
88
89
            // Create task for the duration of this cycle
90 2
            $task_title = $this->_deliverable->get_cycle_identifier($this_cycle_start);
91 2
            $new_task = $this->create_task($this_cycle_start, $next_cycle_start - 1, $task_title, $last_task);
92
        }
93
94
        // TODO: Warehouse management: create new order
95 7
        if (   $this->_deliverable->end < $next_cycle_start
96 7
            && $this->_deliverable->end != 0) {
97 1
            debug_add('Do not register next cycle, the contract ends before');
98 1
            return $this->_deliverable->end_subscription();
99
        }
100
101 7
        if (!$this->_create_at_entry($cycle_number + 1, $next_cycle_start)) {
0 ignored issues
show
Bug introduced by
It seems like $next_cycle_start can also be of type false; however, parameter $start of org_openpsa_invoices_scheduler::_create_at_entry() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

101
        if (!$this->_create_at_entry($cycle_number + 1, /** @scrutinizer ignore-type */ $next_cycle_start)) {
Loading history...
102
            return false;
103
        }
104 7
        if ($send_invoice) {
105 7
            $data = [
106 7
                'cycle_number' => $cycle_number,
107 7
                'next_run' => $next_cycle_start,
108 7
                'invoiced_sum' => $this_cycle_amount,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $this_cycle_amount does not seem to be defined for all execution paths leading up to this point.
Loading history...
109 7
                'tasks_completed' => $tasks_completed,
110 7
                'tasks_not_completed' => $tasks_not_completed
111 7
            ];
112 7
            $this->_notify_owner($calculator, $new_task, $data);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $calculator does not seem to be defined for all execution paths leading up to this point.
Loading history...
113
        }
114 7
        return true;
115
    }
116
117 8
    private function _create_at_entry(int $cycle_number, int $start) : bool
118
    {
119 8
        $args = [
120 8
            'deliverable' => $this->_deliverable->guid,
121 8
            'cycle'       => $cycle_number,
122 8
        ];
123 8
        $at_entry = new midcom_services_at_entry_dba();
124 8
        $at_entry->start = $start;
125 8
        $at_entry->component = 'org.openpsa.sales';
126 8
        $at_entry->method = 'new_subscription_cycle';
127 8
        $at_entry->arguments = $args;
128
129 8
        if (!$at_entry->create()) {
130
            debug_add('AT registration failed, last midgard error was: ' . midcom_connection::get_error_string(), MIDCOM_LOG_WARN);
131
            return false;
132
        }
133 8
        debug_add('AT entry for cycle ' . $cycle_number . ' created');
134 8
        org_openpsa_relatedto_plugin::create($at_entry, 'midcom.services.at', $this->_deliverable, 'org.openpsa.sales');
135 8
        return true;
136
    }
137
138 7
    private function _notify_owner(org_openpsa_invoices_calculator $calculator, ?org_openpsa_projects_task_dba $new_task, array $data)
139
    {
140 7
        $siteconfig = org_openpsa_core_siteconfig::get_instance();
141 7
        $message = [];
142 7
        $salesproject = org_openpsa_sales_salesproject_dba::get_cached($this->_deliverable->salesproject);
143
        try {
144 7
            $owner = midcom_db_person::get_cached($salesproject->owner);
0 ignored issues
show
Bug Best Practice introduced by
The property owner does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
145
        } catch (midcom_error $e) {
146
            $e->log();
147
            return;
148
        }
149 7
        $customer = $salesproject->get_customer();
0 ignored issues
show
Bug introduced by
The method get_customer() does not exist on midcom_core_dbaobject. It seems like you code against a sub-type of midcom_core_dbaobject such as org_openpsa_invoices_invoice_dba or org_openpsa_sales_salesproject_dba. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

149
        /** @scrutinizer ignore-call */ 
150
        $customer = $salesproject->get_customer();
Loading history...
150 7
        $l10n = $this->_i18n->get_l10n('org.openpsa.sales');
151 7
        if ($data['next_run'] === null) {
152
            $next_run_label = $l10n->get('no more cycles');
153
        } else {
154 7
            $next_run_label = $l10n->get_formatter()->date($data['next_run']);
155
        }
156
157
        // Title for long notifications
158 7
        $message['title'] = sprintf($l10n->get('subscription cycle %d closed for agreement %s (%s)'), $data['cycle_number'], $this->_deliverable->title, $customer->get_label());
159
160
        // Content for long notifications
161 7
        $message['content'] = "{$message['title']}\n\n";
162 7
        $message['content'] .= $l10n->get('invoiced') . ': ' . $l10n->get_formatter()->number($data['invoiced_sum']) . "\n\n";
163
164 7
        if ($data['invoiced_sum'] > 0) {
165 4
            $invoice = $calculator->get_invoice();
166 4
            $message['content'] .= $this->_l10n->get('invoice') . " {$invoice->number}:\n";
167 4
            $url = $siteconfig->get_node_full_url('org.openpsa.invoices');
168 4
            $message['content'] .= $url . 'invoice/' . $invoice->guid . "/\n\n";
0 ignored issues
show
Bug introduced by
Are you sure $url of type false|mixed|null can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
            $message['content'] .= /** @scrutinizer ignore-type */ $url . 'invoice/' . $invoice->guid . "/\n\n";
Loading history...
169
        }
170
171 7
        $message['content'] .= $this->render_task_info($l10n->get('tasks completed'), $data['tasks_completed']);
172 7
        $message['content'] .= $this->render_task_info($l10n->get('tasks not completed'), $data['tasks_not_completed']);
173
174 7
        if ($new_task) {
175 2
            $message['content'] .= "\n" . $l10n->get('created new task') . ":\n";
176 2
            $message['content'] .= "{$new_task->title}\n";
177
        }
178
179 7
        $message['content'] .= "\n" . $l10n->get('next run') . ": {$next_run_label}\n\n";
180 7
        $message['content'] .= $this->_i18n->get_string('agreement', 'org.openpsa.projects') . ":\n";
181
182 7
        $url = $siteconfig->get_node_full_url('org.openpsa.sales');
183 7
        $message['content'] .= $url . 'deliverable/' . $this->_deliverable->guid . '/';
184
185
        // Content for short notifications
186 7
        $message['abstract'] = sprintf(
187 7
            $l10n->get('%s: closed subscription cycle %d for agreement %s. invoiced %d. next cycle %s'),
188 7
            $customer->get_label(),
189 7
            $data['cycle_number'],
190 7
            $this->_deliverable->title,
191 7
            $data['invoiced_sum'],
192 7
            $next_run_label);
193
194
        // Send the message out
195 7
        org_openpsa_notifications::notify('org.openpsa.sales:new_subscription_cycle', $owner->guid, $message);
196
    }
197
198 7
    private function render_task_info(string $label, array $tasks) : string
199
    {
200 7
        $content = '';
201 7
        if (!empty($tasks)) {
202 2
            $content .= "\n{$label}:\n";
203
204 2
            foreach ($tasks as $task) {
205 2
                $content .= "{$task->title}: {$task->reportedHours}h\n";
206
            }
207
        }
208 7
        return $content;
209
    }
210
211
    /**
212
     * @todo Check if we already have an open task for this delivery?
213
     */
214 3
    public function create_task(int $start, int $end, string $title, org_openpsa_projects_task_dba $source_task = null) : org_openpsa_projects_task_dba
215
    {
216
        // Create the task
217 3
        $task = new org_openpsa_projects_task_dba();
218 3
        $task->title = $title;
219 3
        $task->start = $start;
220 3
        $task->end = $end;
221
        // TODO: Figure out if we really want to keep this
222 3
        $task->hoursInvoiceableDefault = true;
223
224 3
        $task->agreement = $this->_deliverable->id;
225 3
        $task->description = $this->_deliverable->description;
226 3
        $task->plannedHours = $this->_deliverable->plannedUnits;
227
228 3
        $salesproject = org_openpsa_sales_salesproject_dba::get_cached($this->_deliverable->salesproject);
229 3
        $task->customer = $salesproject->customer;
0 ignored issues
show
Bug Best Practice introduced by
The property customer does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
230 3
        $task->manager = $salesproject->owner;
0 ignored issues
show
Bug Best Practice introduced by
The property owner does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
231
232 3
        $project = $salesproject->get_project();
0 ignored issues
show
Bug introduced by
The method get_project() does not exist on midcom_core_dbaobject. Did you maybe mean get_parent()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

232
        /** @scrutinizer ignore-call */ 
233
        $project = $salesproject->get_project();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
233 3
        $task->project = $project->id;
234 3
        $task->orgOpenpsaAccesstype = $project->orgOpenpsaAccesstype;
235 3
        $task->orgOpenpsaOwnerWg = $project->orgOpenpsaOwnerWg;
236
237 3
        if (!empty($source_task)) {
238 3
            $task->priority = $source_task->priority;
239 3
            $task->manager = $source_task->manager;
240 3
            $task->hoursInvoiceableDefault = $source_task->hoursInvoiceableDefault;
241
        }
242
243 3
        if (!$task->create()) {
244
            throw new midcom_error("The task for this cycle could not be created. Last Midgard error was: " . midcom_connection::get_error_string());
245
        }
246 3
        $task->add_members('contacts', array_keys($salesproject->contacts));
0 ignored issues
show
Bug Best Practice introduced by
The property contacts does not exist on midcom_core_dbaobject. Since you implemented __get, consider adding a @property annotation.
Loading history...
247 3
        if (!empty($source_task)) {
248 3
            $source_task->get_members();
249 3
            $task->add_members('resources', array_keys($source_task->resources));
250
        }
251
252
        // Copy tags from deliverable so we can seek resources
253 3
        $tagger = new net_nemein_tag_handler();
254 3
        $tagger->copy_tags($this->_deliverable, $task);
255
256 3
        midcom::get()->uimessages->add($this->_i18n->get_string('org.openpsa.sales', 'org.openpsa.sales'), sprintf($this->_i18n->get_string('created task "%s"', 'org.openpsa.sales'), $task->title));
257 3
        return $task;
258
    }
259
260
    /**
261
     * Calculcate remaining cycles until salesproject's end or the specified number of months passes
262
     *
263
     * @param integer $months The maximum number of months to look forward
264
     * @param integer $start The timestamp from which to begin
265
     */
266 13
    public function calculate_cycles(int $months = null, int $start = null) : int
267
    {
268 13
        $start ??= time();
269 13
        $cycles = 0;
270 13
        $cycle_time = $this->_deliverable->start;
271 13
        $end_time = $this->_deliverable->end;
272
273
        // This takes care of invalid/unsupported unit configs
274 13
        if ($this->calculate_cycle_next($cycle_time) === false) {
275
            return $cycles;
276
        }
277
278 13
        while ($cycle_time < $start) {
279 8
            $cycle_time = $this->calculate_cycle_next($cycle_time);
280
        }
281
282 13
        if ($months !== null) {
283 9
            $end_time = mktime(date('H', $cycle_time), date('m', $cycle_time), date('i', $cycle_time), date('m', $cycle_time) + $months, date('d', $cycle_time), date('Y', $cycle_time));
0 ignored issues
show
Bug introduced by
date('i', $cycle_time) of type string is incompatible with the type integer expected by parameter $second of mktime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
            $end_time = mktime(date('H', $cycle_time), date('m', $cycle_time), /** @scrutinizer ignore-type */ date('i', $cycle_time), date('m', $cycle_time) + $months, date('d', $cycle_time), date('Y', $cycle_time));
Loading history...
Bug introduced by
date('Y', $cycle_time) of type string is incompatible with the type integer expected by parameter $year of mktime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
            $end_time = mktime(date('H', $cycle_time), date('m', $cycle_time), date('i', $cycle_time), date('m', $cycle_time) + $months, date('d', $cycle_time), /** @scrutinizer ignore-type */ date('Y', $cycle_time));
Loading history...
Bug introduced by
date('m', $cycle_time) of type string is incompatible with the type integer expected by parameter $minute of mktime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
            $end_time = mktime(date('H', $cycle_time), /** @scrutinizer ignore-type */ date('m', $cycle_time), date('i', $cycle_time), date('m', $cycle_time) + $months, date('d', $cycle_time), date('Y', $cycle_time));
Loading history...
Bug introduced by
It seems like $cycle_time can also be of type false; however, parameter $timestamp of date() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
            $end_time = mktime(date('H', /** @scrutinizer ignore-type */ $cycle_time), date('m', $cycle_time), date('i', $cycle_time), date('m', $cycle_time) + $months, date('d', $cycle_time), date('Y', $cycle_time));
Loading history...
Bug introduced by
date('H', $cycle_time) of type string is incompatible with the type integer expected by parameter $hour of mktime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
            $end_time = mktime(/** @scrutinizer ignore-type */ date('H', $cycle_time), date('m', $cycle_time), date('i', $cycle_time), date('m', $cycle_time) + $months, date('d', $cycle_time), date('Y', $cycle_time));
Loading history...
Bug introduced by
date('d', $cycle_time) of type string is incompatible with the type integer expected by parameter $day of mktime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
            $end_time = mktime(date('H', $cycle_time), date('m', $cycle_time), date('i', $cycle_time), date('m', $cycle_time) + $months, /** @scrutinizer ignore-type */ date('d', $cycle_time), date('Y', $cycle_time));
Loading history...
284
        }
285
286 13
        $cycles = 0;
287 13
        while ($cycle_time < $end_time) {
288 13
            $cycle_time = $this->calculate_cycle_next($cycle_time);
0 ignored issues
show
Bug introduced by
It seems like $cycle_time can also be of type false; however, parameter $time of org_openpsa_invoices_sch...:calculate_cycle_next() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

288
            $cycle_time = $this->calculate_cycle_next(/** @scrutinizer ignore-type */ $cycle_time);
Loading history...
289 13
            if ($cycle_time <= $end_time) {
290 13
                $cycles++;
291
            }
292
        }
293 13
        return $cycles;
294
    }
295
296 21
    public function calculate_cycle_next(int $time)
297
    {
298 21
        $date = new DateTime('@' . $time);
299 21
        $date->setTimezone(new DateTimeZone(date_default_timezone_get()));
300 21
        $date->setTime(0, 0, 0);
301
302 21
        switch ($this->_deliverable->unit) {
303 21
            case 'm':
304
                // Monthly recurring subscription
305 14
                $new_date = $this->_add_month($date, 1);
306 14
                break;
307 7
            case 'q':
308
                // Quarterly recurring subscription
309 3
                $new_date = $this->_add_month($date, 3);
310 3
                break;
311 4
            case 'hy':
312
                // Half-yearly recurring subscription
313 1
                $new_date = $this->_add_month($date, 6);
314 1
                break;
315 3
            case 'y':
316
                // Yearly recurring subscription
317 2
                $new_date = clone $date;
318 2
                $new_date->modify('+1 year');
319 2
                break;
320
            default:
321 1
                debug_add('Unrecognized unit value "' . $this->_deliverable->unit . '" for deliverable ' . $this->_deliverable->guid . ", returning false", MIDCOM_LOG_WARN);
322 1
                return false;
323
        }
324
325
        //If previous cycle was run at the end of the month, the new one should be at the end of the month as well
326 20
        if (   $date->format('t') == $date->format('j')
327 20
            && $new_date->format('t') != $new_date->format('j')) {
328
            $new_date->setDate((int) $new_date->format('Y'), (int) $new_date->format('m'), (int) $new_date->format('t'));
329
        }
330 20
        return (int) $new_date->format('U');
331
    }
332
333
    /**
334
     * Workaround for odd PHP DateTime behavior where for example
335
     * 2012-10-31 + 1 month would return 2012-12-01. This function makes
336
     * sure the new date is always in the expected month (so in the example above
337
     * it would return 2012-11-30)
338
     *
339
     * @param Datetime $orig Original timestamp
340
     * @param integer $offset number of months to add
341
     */
342 18
    private function _add_month(Datetime $orig, int $offset) : DateTime
343
    {
344 18
        $new_date = clone $orig;
345 18
        $new_date->modify('+' . $offset . ' months');
346 18
        $control = clone $new_date;
347 18
        $control->modify('-' . $offset . ' months');
348
349 18
        while ($orig->format('m') !== $control->format('m')) {
350 1
            $new_date->modify('-1 day');
351 1
            $control = clone $new_date;
352 1
            $control->modify('-' . $offset . ' months');
353
        }
354
355 18
        return $new_date;
356
    }
357
358 10
    public function get_cycle_start(int $cycle_number, int $time)
359
    {
360 10
        if ($cycle_number == 1) {
361 7
            if ($this->subscription_day) {
362 3
                return gmmktime(0, 0, 0, date('n', $time) + 1, $this->subscription_day, date('Y', $time));
0 ignored issues
show
Bug introduced by
date('Y', $time) of type string is incompatible with the type integer expected by parameter $year of gmmktime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

362
                return gmmktime(0, 0, 0, date('n', $time) + 1, $this->subscription_day, /** @scrutinizer ignore-type */ date('Y', $time));
Loading history...
363
            }
364
365
            // no explicit day of month set for invoicing, use the deliverable start date
366 4
            return $this->_deliverable->start;
367
        }
368
369
        // cycle number > 1
370 4
        return $time;
371
    }
372
}
373