Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.29% covered (success)
94.29%
33 / 35
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
PrefixMetadata
94.12% covered (success)
94.12%
32 / 34
66.67% covered (warning)
66.67%
2 / 3
20.08
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
 fromArray
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
9
 shouldReset
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
10.46
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Domain\Model;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use DateTimeImmutable;
12
13/**
14 * Prefix Metadata Value Object.
15 *
16 * Immutable representation of a record prefix configuration rule from a_core_prefix_records.
17 * Encapsulates sequence parameters, template pattern, reset strategy, and padding rules.
18 *
19 * @package App\Core\Engine\Domain\Model
20 */
21final readonly class PrefixMetadata
22{
23    /**
24     * PrefixMetadata constructor.
25     *
26     * @param int         $id              Prefix primary key.
27     * @param int         $moduleId        Target module ID.
28     * @param int         $fieldId         Target field ID within the module.
29     * @param string      $name            Descriptive configuration label.
30     * @param string      $prefix          Static prefix text.
31     * @param string      $postfix         Static postfix text.
32     * @param string|null $pattern         Dynamic template pattern (e.g. {PREFIX}{YYYY}{NUMBER}{POSTFIX}).
33     * @param int         $leadingZeros    Number of digits with zero-padding (0 for no padding).
34     * @param int         $startNumber     Initial starting sequence number.
35     * @param int         $currentNumber   Current sequence number.
36     * @param int         $step            Increment step per record (default 1).
37     * @param string      $resetFrequency  Reset policy: 'never', 'yearly', 'monthly', 'daily'.
38     * @param string|null $lastResetDate   Timestamp of the last sequence increment/reset.
39     * @param int|null    $picklistFieldId Optional picklist field ID for dictionary-scoped numbering.
40     * @param bool        $isActive        Whether the prefix rule is active.
41     * @param string|null $description     Optional administrator notes.
42     */
43    public function __construct(
44        public int     $id,
45        public int     $moduleId,
46        public int     $fieldId,
47        public string  $name,
48        public string  $prefix = '',
49        public string  $postfix = '',
50        public ?string $pattern = null,
51        public int     $leadingZeros = 0,
52        public int     $startNumber = 1,
53        public int     $currentNumber = 1,
54        public int     $step = 1,
55        public string  $resetFrequency = 'never',
56        public ?string $lastResetDate = null,
57        public ?int    $picklistFieldId = null,
58        public bool    $isActive = true,
59        public ?string $description = null,
60    ) {
61    }
62
63    /**
64     * Hydrates a PrefixMetadata instance from a database associative array.
65     *
66     * @param array<string, mixed> $data Raw database row.
67     * @return self Hydrated metadata value object.
68     */
69    public static function fromArray(array $data): self
70    {
71        return new self(
72            id: (int) ($data['id'] ?? 0),
73            moduleId: (int) ($data['module_id'] ?? 0),
74            fieldId: (int) ($data['field_id'] ?? 0),
75            name: (string) ($data['name'] ?? ''),
76            prefix: (string) ($data['prefix'] ?? ''),
77            postfix: (string) ($data['postfix'] ?? ''),
78            pattern: isset($data['pattern']) && $data['pattern'] !== '' ? (string) $data['pattern'] : null,
79            leadingZeros: (int) ($data['leading_zeros'] ?? 0),
80            startNumber: (int) ($data['start_number'] ?? 1),
81            currentNumber: (int) ($data['current_number'] ?? 1),
82            step: max(1, (int) ($data['step'] ?? 1)),
83            resetFrequency: (string) ($data['reset_frequency'] ?? 'never'),
84            lastResetDate: isset($data['last_reset_date']) && $data['last_reset_date'] !== ''
85                ? (string) $data['last_reset_date'] : null,
86            picklistFieldId: isset($data['picklist_field_id']) && (int) $data['picklist_field_id'] > 0
87                ? (int) $data['picklist_field_id'] : null,
88            isActive: (bool) ($data['is_active'] ?? true),
89            description: isset($data['description']) && $data['description'] !== ''
90                ? (string) $data['description'] : null,
91        );
92    }
93
94    /**
95     * Checks whether the sequence should be reset based on the current timestamp.
96     *
97     * @param DateTimeImmutable $now Current point in time.
98     * @return bool True if a reset cycle has elapsed.
99     */
100    public function shouldReset(DateTimeImmutable $now): bool
101    {
102        if ($this->resetFrequency === 'never' || $this->lastResetDate === null || $this->lastResetDate === '') {
103            return false;
104        }
105
106        $last = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $this->lastResetDate)
107            ?: DateTimeImmutable::createFromFormat('Y-m-d', substr($this->lastResetDate, 0, 10));
108
109        if ($last === false) {
110            return false;
111        }
112
113        return match ($this->resetFrequency) {
114            'yearly'  => $last->format('Y') !== $now->format('Y'),
115            'monthly' => $last->format('Y-m') !== $now->format('Y-m'),
116            'daily'   => $last->format('Y-m-d') !== $now->format('Y-m-d'),
117            default   => false,
118        };
119    }
120}