Builder::reserved3()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 5
b 0
f 0
nc 6
nop 1
dl 0
loc 17
ccs 10
cts 10
cp 1
crap 4
rs 10
1
<?php
2
3
namespace MarcinOrlowski\QrcodeFormatter;
4
5
/**
6
 * Bank QrCode Formatter
7
 *
8
 * @package   MarcinOrlowski\QrcodeFormatter
9
 *
10
 * @author    Marcin Orlowski <mail (#) marcinOrlowski (.) com>
11
 * @copyright 2020 Marcin Orlowski
12
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
13
 * @link      https://github.com/MarcinOrlowski/bank-qrcode-formatter
14
 */
15
16
use InvalidArgumentException;
17
use OutOfRangeException;
18
use RuntimeException;
19
20
class Builder
21
{
22
	/** @var int */
23
	const TYPE_COMPANY = 1;
24
25
	/** @var int */
26
	const TYPE_PERSON = 2;
27
28
	/** @var string */
29
	protected $separator = '|';
30
31
	/** @var int Maks. allowed length of result string */
32
	const MAX_LEN = 160;
33
34
35
	/**
36
	 * Builder constructor.
37
	 *
38
	 * @param int  $recipient_type
39
	 * @param bool $strict_mode
40
	 */
41 131
	public function __construct($recipient_type = self::TYPE_PERSON, $strict_mode = false)
42
	{
43 131
		if ($recipient_type !== self::TYPE_COMPANY && $recipient_type !== self::TYPE_PERSON) {
44 1
			throw new RuntimeException('Invalid recipient type specified.');
45
		}
46
47 130
		$this->recipient_type = $recipient_type;
48 130
		$this->strictMode($strict_mode);
49 130
	}
50
51
	/** @var bool */
52
	protected $strict_mode = false;
53
54
	/**
55
	 * Controls strict mode. When mode is disabled (default) some methods may trim down string arguments
56
	 * exceeding max allowed length. With strict mode on, such case would throw InvalidArgumentException.
57
	 *
58
	 * @param bool $mode Set to @true to enable strict mode, @false (default) otherwise.
59
	 */
60 130
	public function strictMode($mode)
61
	{
62 130
		if (!is_bool($mode)) {
63 3
			throw new InvalidArgumentException('Mode argument must be a boolean.');
64
		}
65
66 130
		$this->strict_mode = (bool)$mode;
67 130
	}
68
69
	/** @var int */
70
	protected $recipient_type = self::TYPE_PERSON;
71
72
	/** @var string VAT ID (10 chars). required for TYPE_COMPANY (VAT ID), optional otherwise, digits */
73
	protected $vat_id = '';
74
75
	/**
76
	 * Sets recipient Vat ID
77
	 *
78
	 * @param string|int|null $vat_id
79
	 *
80
	 * @return $this
81
	 */
82 28
	public function vatId($vat_id)
83
	{
84 28
		$this->vat_id = $this->validateVatId($vat_id);
85
86 6
		return $this;
87
	}
88
89
	/**
90
	 * @param string|int|null $vat_id
91
	 *
92
	 * @return string
93
	 *
94
	 * @throws \InvalidArgumentException
95
	 * @throws \RuntimeException
96
	 */
97 30
	protected function validateVatId($vat_id)
98
	{
99 30
		if (is_string($vat_id)) {
100 27
			$vat_id = trim(str_replace('-', '', $vat_id));
101 30
		} elseif ($vat_id === null) {
102 2
			$vat_id = '';
103 4
		} elseif (is_int($vat_id)) {
104 1
			$vat_id = sprintf('%010d', $vat_id);
105 1
		} else {
106 1
			throw new InvalidArgumentException('VatId can either be a string, int or null.');
107
		}
108
109 29
		if ($vat_id !== '') {
110 24
			if (preg_match('/^\d{10}$/', $vat_id) !== 1) {
111 19
				throw new InvalidArgumentException("Invalid VAT ID set. Must be contain 10 chars, digits only. '{$vat_id}' provided.");
112
			}
113 5
		}
114
115 10
		if ($this->recipient_type === self::TYPE_COMPANY && $vat_id === '') {
116 3
			throw new RuntimeException('Company recipient must have VAT ID set.');
117
		}
118
119 7
		return $vat_id;
120
	}
121
122
	/** @var string Recipient bank account number (26 digits), mandatory */
123
	protected $bank_account = '';
124
125
	/**
126
	 * Sets mandatory recipient routing bank account number. Account number must contain 26 digits.
127
	 * Digits can be grouped and separated by spaces however all spaces will be removed.
128
	 *
129
	 * @param string $account Recipient bank account number (26 digits)
130
	 *
131
	 * @return $this
132
	 */
133 30
	public function bankAccount($account)
134
	{
135 30
		$this->bank_account = $this->validateBankAccount($account);
136
137 6
		return $this;
138
	}
139
140
	/**
141
	 * @param string $account
142
	 *
143
	 * @return string
144
	 *
145
	 * @throws \InvalidArgumentException
146
	 */
147 30
	protected function validateBankAccount($account)
148
	{
149 30
		if (!is_string($account)) {
150 4
			throw new InvalidArgumentException('Bank account number must be a string.');
151
		}
152 26
		$account = str_replace(' ', '', $account);
153
154 26
		if (preg_match('/^\d{26}$/', $account) !== 1) {
155 20
			throw new InvalidArgumentException("Bank account number must be 26 chars long, digits only. '{$account}' provided.");
156
		}
157
158 6
		return $account;
159
	}
160
161
	/** @var string 20 chars max, recipient name, mandatory */
162
	protected $recipient_name = '';
163
164
	/** @var int */
165
	const NAME_MAX_LEN = 20;
166
167
	/**
168
	 * Sets recipient name. Up to 20 chars (longer strings are allowed and will be trimmed).
169
	 *
170
	 * @param string $name recipient name
171
	 *
172
	 * @return $this
173
	 */
174 13
	public function name($name)
175
	{
176 13
		$this->recipient_name = $this->validateName($name);
177
178 7
		return $this;
179
	}
180
181
	/**
182
	 * @param string $name
183
	 *
184
	 * @return string
185
	 *
186
	 * @throws \InvalidArgumentException
187
	 * @throws \RuntimeException
188
	 */
189 13
	protected function validateName($name)
190
	{
191 13
		if (!is_string($name)) {
192 4
			throw new InvalidArgumentException('Recipient name must be a string.');
193
		}
194
195 9
		if ($this->strict_mode && mb_strlen($name) > self::NAME_MAX_LEN) {
196 1
			throw new InvalidArgumentException(sprintf('Recipient name must not exceed %d chars.', self::NAME_MAX_LEN));
197
		}
198
199 8
		$name = mb_substr(trim($name), 0, self::NAME_MAX_LEN);
200
201 8
		if ($name === '') {
202 1
			throw new RuntimeException('Recipient name cannot be empty.');
203
		}
204
205 7
		return $name;
206
	}
207
208
	/** @var string 2 chars, country code (i.e. 'PL'), optional, letters */
209
	protected $country_code = '';
210
211
	/**
212
	 * @param string|null $country_code 2 chars, country code (i.e. 'PL'), optional, letters
213
	 *
214
	 * @return $this
215
	 *
216
	 * @throws \InvalidArgumentException
217
	 */
218 11
	public function country($country_code)
219
	{
220 11
		if ($country_code === null) {
221 1
			$country_code = '';
222 1
		}
223
224 11
		if (!is_string($country_code)) {
225 3
			throw new InvalidArgumentException('Country code must be a string.');
226
		}
227
228 8
		$country_code = mb_strtoupper($country_code);
229 8
		if ($country_code !== '') {
230 7
			if (preg_match('/^[A-Z]{2}$/', $country_code) !== 1) {
231 3
				throw new InvalidArgumentException("Country code must be a 2 character long, letters only. '{$country_code}' provided.");
232
			}
233 4
		}
234
235 5
		$this->country_code = strtoupper($country_code);
236
237 5
		return $this;
238
	}
239
240
	/** @var string */
241
	protected $payment_title = '';
242
243
	/** @var int */
244
	const TITLE_MAX_LEN = 32;
245
246
	/**
247
	 * @param string $title 32 chars, payment title, mandatory, letters+digits
248
	 *
249
	 * @return $this
250
	 */
251 13
	public function title($title)
252
	{
253 13
		$this->payment_title = $this->validateTitle($title);
254
255 7
		return $this;
256
	}
257
258
	/**
259
	 * @param string $title
260
	 *
261
	 * @return string
262
	 *
263
	 * @throws \InvalidArgumentException
264
	 */
265 13
	protected function validateTitle($title)
266
	{
267 13
		if (!is_string($title)) {
268 4
			throw new InvalidArgumentException('Payment title must be a string.');
269
		}
270
271 9
		if ($this->strict_mode && mb_strlen($title) > self::TITLE_MAX_LEN) {
272 1
			throw new InvalidArgumentException(sprintf('Payment title must not exceed %d chars.', self::TITLE_MAX_LEN));
273
		}
274
275 8
		if ($title === '') {
276 1
			throw new RuntimeException('Payment title cannot be empty.');
277
		}
278
279 7
		return mb_substr(trim($title), 0, self::TITLE_MAX_LEN);
280
	}
281
282
	/** @var int|null */
283
	protected $amount = null;
284
285
	/**
286
	 * @param float|int $amount 6 chars, amount in Polish grosz, digits, mandatory
287
	 *
288
	 * @return $this
289
	 */
290 29
	public function amount($amount)
291
	{
292 29
		$this->amount = $this->validateAmount($amount);
293
294 24
		return $this;
295
	}
296
297
	/**
298
	 * @param float|int $amount
299
	 *
300
	 * @return int
301
	 *
302
	 * @throws \OutOfRangeException
303
	 * @throws \InvalidArgumentException
304
	 * @throws \RuntimeException
305
	 */
306 29
	protected function validateAmount($amount)
307
	{
308 29
		if ($amount === null) {
309 1
			throw new RuntimeException('Amount not specified.');
310
		}
311
312 28
		if (is_float($amount)) {
313 11
			$amount = (int)($amount * 100);
314 28
		} elseif (!is_int($amount)) {
315 2
			throw new InvalidArgumentException('Amount must be either float or int');
316
		}
317
318 26
		if ($amount < 0) {
319 1
			throw new OutOfRangeException('Amount cannot be negative.');
320
		}
321
322 25
		if ($amount > 999999) {
323 1
			throw new OutOfRangeException('Amount representation cannot exceed 6 digits. Current value: {$amount}');
324
		}
325
326 24
		return $amount;
327
	}
328
329
	/** @var string */
330
	protected $reserved1 = '';
331
332
	/** @var int */
333
	const RESERVED1_MAX_LEN = 20;
334
335
	/**
336
	 * @param string|null $id 20 chars, reserved i.e. for payment reference id, optional, digits (but we use letters+digits as some banks do too)
337
	 *
338
	 * @return $this
339
	 *
340
	 * @throws \InvalidArgumentException
341
	 */
342 11
	public function reserved1($id)
343
	{
344 11
		if ($id === null) {
345 1
			$id = '';
346 1
		}
347
348 11
		if (!is_string($id)) {
349 6
			throw new InvalidArgumentException('Reserved1/RefId value must be a string.');
350
		}
351
352 5
		if (mb_strlen($id) > self::RESERVED1_MAX_LEN) {
353 1
			throw new InvalidArgumentException(sprintf('Maksymalna długość wartości Reserved1/RefId to %d znaków.', self::RESERVED1_MAX_LEN));
354
		}
355
356 4
		$this->reserved1 = $id;
357
358 4
		return $this;
359
	}
360
361
	/**
362
	 * Alias for reserved1()
363
	 *
364
	 * @param string|null $id
365
	 *
366
	 * @return $this
367
	 */
368 4
	public function refId($id)
369
	{
370 4
		return $this->reserved1($id);
371
	}
372
373
	/** @var string */
374
	protected $reserved2 = '';
375
376
	/** @var int */
377
	const RESERVED2_MAX_LEN = 12;
378
379
	/**
380
	 * 12 chars, reserved i.e. for Invobill reference id, optional, digits (but we allow letters+digits as some banks do too)
381
	 *
382
	 * @param string|null $id
383
	 *
384
	 * @return $this
385
	 *
386
	 * @throws \InvalidArgumentException
387
	 */
388 6
	public function reserved2($id)
389
	{
390 6
		if ($id === null) {
391 1
			$id = '';
392 1
		}
393
394 6
		if (!is_string($id)) {
395 3
			throw new InvalidArgumentException('Reserved2 value must be a string.');
396
		}
397
398 3
		if (mb_strlen($id) > self::RESERVED2_MAX_LEN) {
399 1
			throw new InvalidArgumentException(sprintf('Maksymalna długość wartości Reserved2 to %d znaków.', self::RESERVED2_MAX_LEN));
400
		}
401
402 2
		$this->reserved2 = $id;
403
404 2
		return $this;
405
	}
406
407
	/** @var string */
408
	protected $reserved3 = '';
409
410
	/** @var int */
411
	const RESERVED3_MAX_LEN = 24;
412
413
	/**
414
	 * 24 chars, reserved, optional, letters+digits
415
	 *
416
	 * @param string|null $id
417
	 *
418
	 * @return $this
419
	 *
420
	 * @throws \InvalidArgumentException
421
	 */
422 6
	public function reserved3($id)
423
	{
424 6
		if ($id === null) {
425 1
			$id = '';
426 1
		}
427
428 6
		if (!is_string($id)) {
429 3
			throw new InvalidArgumentException('Reserved3 value must be a string.');
430
		}
431
432 3
		if (mb_strlen($id) > self::RESERVED3_MAX_LEN) {
433 1
			throw new InvalidArgumentException(sprintf('Maksymalna długość wartości Reserved3 to %d znaków.', self::RESERVED3_MAX_LEN));
434
		}
435
436 2
		$this->reserved3 = $id;
437
438 2
		return $this;
439
	}
440
441
	/**
442
	 * @return string
443
	 *
444
	 * @throws \RuntimeException
445
	 */
446 4
	public function build()
447
	{
448
		// validate
449 4
		$this->validateBankAccount($this->bank_account);
450 4
		$this->validateName($this->recipient_name);
451 4
		$this->validateVatId($this->vat_id);
452 3
		$this->validateTitle($this->payment_title);
453 3
		$this->validateAmount($this->amount);
454
455
		// build
456
		$fields = [
457 3
			$this->vat_id,
458 3
			$this->country_code,
459 3
			$this->bank_account,
460 3
			sprintf('%06d', $this->amount),
461 3
			$this->recipient_name,
462 3
			$this->payment_title,
463 3
			$this->reserved1,
464 3
			$this->reserved2,
465 3
			$this->reserved3,
466 3
		];
467
468 3
		$result = implode($this->separator, $fields);
469 3
		if (mb_strlen($result) > self::MAX_LEN) {
470 1
			throw new RuntimeException(
471 1
				sprintf('Oops, this should not happen! Result string is %d chars long (max allowed %d). Please report this!', mb_strlen($result), self::MAX_LEN));
472
		}
473
474 2
		return $result;
475
	}
476
}
477