1
"""The higher-level objects API."""
2

3 4
import collections
4 4
import datetime
5 4
from decimal import Decimal
6 4
from typing import Iterable, List, Mapping, Optional, Union
7

8 4
import attr
9 4
from attr.validators import instance_of, optional
10

11 4
import netsgiro
12 4
import netsgiro.records
13 4
from netsgiro.records import Record
14 4
from netsgiro.validators import str_of_length
15

16

17 4
__all__ = [
18
    'Transmission',
19
    'Assignment',
20
    'Agreement',
21
    'PaymentRequest',
22
    'Transaction',
23
    'parse',
24
]
25

26

27 4
@attr.s
28 1
class Transmission:
29
    """Transmission is the top-level object.
30

31
    An OCR file contains a single transmission. The transmission can contain
32
    multiple :class:`~netsgiro.Assignment` objects of various types.
33
    """
34

35
    #: Data transmitters unique enumeration of the transmission. String of 7
36
    #: digits.
37 4
    number = attr.ib(validator=str_of_length(7))
38

39
    #: Data transmitter's Nets ID. String of 8 digits.
40 4
    data_transmitter = attr.ib(validator=str_of_length(8))
41

42
    #: Data recipient's Nets ID. String of 8 digits.
43 4
    data_recipient = attr.ib(validator=str_of_length(8))
44

45
    #: For OCR Giro files from Nets, this is Nets' processing date.
46
    #:
47
    #: For AvtaleGiro payment request, the earliest due date in the
48
    #: transmission is automatically used.
49 4
    date = attr.ib(default=None, validator=optional(instance_of(datetime.date)))
50

51
    #: List of assignments.
52 4
    assignments = attr.ib(default=attr.Factory(list), repr=False)
53

54 4
    @classmethod
55 4
    def from_records(cls, records: List[Record]) -> 'Transmission':
56
        """Build a Transmission object from a list of record objects."""
57 4
        if len(records) < 2:
58 4
            raise ValueError(
59
                'At least 2 records required, got {}'.format(len(records))
60
            )
61

62 4
        start, body, end = records[0], records[1:-1], records[-1]
63

64 4
        assert isinstance(start, netsgiro.records.TransmissionStart)
65 4
        assert isinstance(end, netsgiro.records.TransmissionEnd)
66

67 4
        return cls(
68
            number=start.transmission_number,
69
            data_transmitter=start.data_transmitter,
70
            data_recipient=start.data_recipient,
71
            date=end.nets_date,
72
            assignments=cls._get_assignments(body),
73
        )
74

75 4
    @staticmethod
76 4
    def _get_assignments(records: List[Record]) -> List['Assignment']:
77 4
        assignments = collections.OrderedDict()
78

79 4
        current_assignment_number = None
80 4
        for record in records:
81 4
            if isinstance(record, netsgiro.records.AssignmentStart):
82 4
                current_assignment_number = record.assignment_number
83 4
                assignments[current_assignment_number] = []
84 4
            if current_assignment_number is None:
85 0
                raise ValueError(
86
                    'Expected AssignmentStart record, got {!r}'.format(record)
87
                )
88 4
            assignments[current_assignment_number].append(record)
89 4
            if isinstance(record, netsgiro.records.AssignmentEnd):
90 4
                current_assignment_number = None
91

92 4
        return [Assignment.from_records(rs) for rs in assignments.values()]
93

94 4
    def to_ocr(self) -> str:
95
        """Convert the transmission to an OCR string."""
96 4
        lines = [record.to_ocr() for record in self.to_records()]
97 4
        return '\n'.join(lines)
98

99 4
    def to_records(self) -> Iterable[Record]:
100
        """Convert the transmission to a list of records."""
101 4
        yield self._get_start_record()
102 4
        for assignment in self.assignments:
103 4
            yield from assignment.to_records()
104 4
        yield self._get_end_record()
105

106 4
    def _get_start_record(self) -> Record:
107 4
        return netsgiro.records.TransmissionStart(
108
            service_code=netsgiro.ServiceCode.NONE,
109
            transmission_number=self.number,
110
            data_transmitter=self.data_transmitter,
111
            data_recipient=self.data_recipient,
112
        )
113

114 4
    def _get_end_record(self) -> Record:
115 4
        avtalegiro_payment_request = all(
116
            assignment.service_code == netsgiro.ServiceCode.AVTALEGIRO
117
            and assignment.type
118
            in (
119
                netsgiro.AssignmentType.TRANSACTIONS,
120
                netsgiro.AssignmentType.AVTALEGIRO_CANCELLATIONS,
121
            )
122
            for assignment in self.assignments
123
        )
124 4
        if self.assignments and avtalegiro_payment_request:
125 4
            date = min(
126
                assignment.get_earliest_transaction_date()
127
                for assignment in self.assignments
128
            )
129
        else:
130 4
            date = self.date
131

132 4
        return netsgiro.records.TransmissionEnd(
133
            service_code=netsgiro.ServiceCode.NONE,
134
            num_transactions=self.get_num_transactions(),
135
            num_records=self.get_num_records(),
136
            total_amount=int(self.get_total_amount() * 100),
137
            nets_date=date,
138
        )
139

140 4
    def add_assignment(
141
        self,
142
        *,
143
        service_code: netsgiro.ServiceCode,
144
        assignment_type: netsgiro.AssignmentType,
145
        agreement_id: Optional[str] = None,
146
        number: str,
147
        account: str,
148
        date: Optional[datetime.date] = None
149
    ) -> 'Assignment':
150
        """Add an assignment to the tranmission."""
151

152 4
        assignment = Assignment(
153
            service_code=service_code,
154
            type=assignment_type,
155
            agreement_id=agreement_id,
156
            number=number,
157
            account=account,
158
            date=date,
159
        )
160 4
        self.assignments.append(assignment)
161 4
        return assignment
162

163 4
    def get_num_transactions(self) -> int:
164
        """Get number of transactions in the transmission."""
165 4
        return sum(
166
            assignment.get_num_transactions() for assignment in self.assignments
167
        )
168

169 4
    def get_num_records(self) -> int:
170
        """Get number of records in the transmission.
171

172
        Includes the transmission's start and end record.
173
        """
174 4
        return 2 + sum(
175
            assignment.get_num_records() for assignment in self.assignments
176
        )
177

178 4
    def get_total_amount(self) -> Decimal:
179
        """Get the total amount from all transactions in the transmission."""
180 4
        return sum(
181
            assignment.get_total_amount() for assignment in self.assignments
182
        )
183

184

185 4
@attr.s
186 1
class Assignment:
187
    """An Assignment groups multiple transactions within a transmission.
188

189
    Use :meth:`netsgiro.Transmission.add_assignment` to create assignments.
190
    """
191

192
    #: The service code. One of :class:`~netsgiro.ServiceCode`.
193 4
    service_code = attr.ib(converter=netsgiro.ServiceCode)
194

195
    #: The transaction type. One of :class:`~netsgiro.TransactionType`.
196 4
    type = attr.ib(converter=netsgiro.AssignmentType)
197

198
    #: The assignment number. String of 7 digits.
199 4
    number = attr.ib(validator=str_of_length(7))
200

201
    #: The payee's bank account. String of 11 digits.
202 4
    account = attr.ib(validator=str_of_length(11))
203

204
    #: Used for OCR Giro.
205
    #:
206
    #: The payee's agreement ID with Nets. String of 9 digits.
207 4
    agreement_id = attr.ib(default=None, validator=optional(str_of_length(9)))
208

209
    #: Used for OCR Giro.
210
    #:
211
    #: The date the assignment was generated by Nets.
212 4
    date = attr.ib(default=None, validator=optional(instance_of(datetime.date)))
213

214
    #: List of transaction objects, like :class:`~netsgiro.Agreement`,
215
    #: :class:`~netsgiro.PaymentRequest`, :class:`~netsgiro.Transaction`.
216 4
    transactions = attr.ib(default=attr.Factory(list), repr=False)
217

218 4
    _next_transaction_number = 1
219

220 4
    @classmethod
221 4
    def from_records(cls, records: List[Record]) -> 'Assignment':
222
        """Build an Assignment object from a list of record objects."""
223 4
        if len(records) < 2:
224 4
            raise ValueError(
225
                'At least 2 records required, got {}'.format(len(records))
226
            )
227

228 4
        start, body, end = records[0], records[1:-1], records[-1]
229

230 4
        assert isinstance(start, netsgiro.records.AssignmentStart)
231 4
        assert isinstance(end, netsgiro.records.AssignmentEnd)
232

233 4
        if start.service_code == netsgiro.ServiceCode.AVTALEGIRO:
234 4
            if (
235
                start.assignment_type
236
                == netsgiro.AssignmentType.AVTALEGIRO_AGREEMENTS
237
            ):
238 4
                transactions = cls._get_agreements(body)
239
            else:
240 4
                transactions = cls._get_payment_requests(body)
241 4
        elif start.service_code == netsgiro.ServiceCode.OCR_GIRO:
242 4
            transactions = cls._get_transactions(body)
243
        else:
244 0
            raise ValueError(
245
                'Unknown service code: {}'.format(start.service_code)
246
            )
247

248 4
        return cls(
249
            service_code=start.service_code,
250
            type=start.assignment_type,
251
            agreement_id=start.agreement_id,
252
            number=start.assignment_number,
253
            account=start.assignment_account,
254
            date=end.nets_date,
255
            transactions=transactions,
256
        )
257

258 4
    @staticmethod
259 4
    def _get_agreements(records: List[Record]) -> List['Agreement']:
260 4
        return [Agreement.from_records([r]) for r in records]
261

262 4
    @classmethod
263 4
    def _get_payment_requests(
264
        cls, records: List[Record]
265
    ) -> List['PaymentRequest']:
266 4
        transactions = cls._group_by_transaction_number(records)
267 4
        return [PaymentRequest.from_records(rs) for rs in transactions.values()]
268

269 4
    @classmethod
270 4
    def _get_transactions(cls, records: List[Record]) -> List['Transaction']:
271 4
        transactions = cls._group_by_transaction_number(records)
272 4
        return [Transaction.from_records(rs) for rs in transactions.values()]
273

274 4
    @staticmethod
275 4
    def _group_by_transaction_number(
276
        records: List[Record],
277
    ) -> Mapping[int, List[Record]]:
278 4
        transactions = collections.OrderedDict()
279

280 4
        for record in records:
281 4
            if record.transaction_number not in transactions:
282 4
                transactions[record.transaction_number] = []
283 4
            transactions[record.transaction_number].append(record)
284

285 4
        return transactions
286

287 4
    def to_records(self) -> Iterable[Record]:
288
        """Convert the assignment to a list of records."""
289 4
        yield self._get_start_record()
290 4
        for transaction in self.transactions:
291 4
            yield from transaction.to_records()
292 4
        yield self._get_end_record()
293

294 4
    def _get_start_record(self) -> Record:
295 4
        return netsgiro.records.AssignmentStart(
296
            service_code=self.service_code,
297
            assignment_type=self.type,
298
            assignment_number=self.number,
299
            assignment_account=self.account,
300
            agreement_id=self.agreement_id,
301
        )
302

303 4
    def _get_end_record(self) -> Record:
304 4
        if self.service_code == netsgiro.ServiceCode.OCR_GIRO:
305 4
            dates = {
306
                'nets_date_1': self.date,
307
                'nets_date_2': self.get_earliest_transaction_date(),
308
                'nets_date_3': self.get_latest_transaction_date(),
309
            }
310 4
        elif self.service_code == netsgiro.ServiceCode.AVTALEGIRO:
311 4
            dates = {
312
                'nets_date_1': self.get_earliest_transaction_date(),
313
                'nets_date_2': self.get_latest_transaction_date(),
314
            }
315
        else:
316 0
            raise ValueError(
317
                'Unhandled service code: {}'.format(self.service_code)
318
            )
319

320 4
        return netsgiro.records.AssignmentEnd(
321
            service_code=self.service_code,
322
            assignment_type=self.type,
323
            num_transactions=self.get_num_transactions(),
324
            num_records=self.get_num_records(),
325
            total_amount=int(self.get_total_amount() * 100),
326
            **dates
327
        )
328

329 4
    def add_payment_request(
330
        self,
331
        *,
332
        kid: str,
333
        due_date: datetime.date,
334
        amount: Decimal,
335
        reference: Optional[str] = None,
336
        payer_name: Optional[str] = None,
337
        bank_notification: Union[bool, str] = False
338
    ) -> 'Transaction':
339
        """Add an AvtaleGiro payment request to the assignment.
340

341
        The assignment must have service code
342
        :attr:`~netsgiro.ServiceCode.AVTALEGIRO` and assignment type
343
        :attr:`~netsgiro.AssignmentType.TRANSACTIONS`.
344
        """
345

346 4
        assert (
347
            self.service_code == netsgiro.ServiceCode.AVTALEGIRO
348
        ), 'Can only add payment requests to AvtaleGiro assignments'
349 4
        assert (
350
            self.type == netsgiro.AssignmentType.TRANSACTIONS
351
        ), 'Can only add payment requests to transaction assignments'
352

353 4
        if bank_notification:
354 4
            transaction_type = (
355
                netsgiro.TransactionType.AVTALEGIRO_WITH_BANK_NOTIFICATION
356
            )
357
        else:
358 4
            transaction_type = (
359
                netsgiro.TransactionType.AVTALEGIRO_WITH_PAYEE_NOTIFICATION
360
            )
361

362 4
        return self._add_avtalegiro_transaction(
363
            transaction_type=transaction_type,
364
            kid=kid,
365
            due_date=due_date,
366
            amount=amount,
367
            reference=reference,
368
            payer_name=payer_name,
369
            bank_notification=bank_notification,
370
        )
371

372 4
    def add_payment_cancellation(
373
        self,
374
        *,
375
        kid: str,
376
        due_date: datetime.date,
377
        amount: Decimal,
378
        reference: Optional[str] = None,
379
        payer_name: Optional[str] = None,
380
        bank_notification: Union[bool, str] = False
381
    ) -> 'Transaction':
382
        """Add an AvtaleGiro cancellation to the assignment.
383

384
        The assignment must have service code
385
        :attr:`~netsgiro.ServiceCode.AVTALEGIRO` and assignment type
386
        :attr:`~netsgiro.AssignmentType.AVTALEGIRO_CANCELLATIONS`.
387

388
        Otherwise, the cancellation must be identical to the payment request it
389
        is cancelling.
390
        """
391

392 0
        assert (
393
            self.service_code == netsgiro.ServiceCode.AVTALEGIRO
394
        ), 'Can only add cancellation to AvtaleGiro assignments'
395 0
        assert (
396
            self.type == netsgiro.AssignmentType.AVTALEGIRO_CANCELLATIONS
397
        ), 'Can only add cancellation to cancellation assignments'
398

399 0
        return self._add_avtalegiro_transaction(
400
            transaction_type=netsgiro.TransactionType.AVTALEGIRO_CANCELLATION,
401
            kid=kid,
402
            due_date=due_date,
403
            amount=amount,
404
            reference=reference,
405
            payer_name=payer_name,
406
            bank_notification=bank_notification,
407
        )
408

409 4
    def _add_avtalegiro_transaction(
410
        self,
411
        *,
412
        transaction_type,
413
        kid,
414
        due_date,
415
        amount,
416
        reference=None,
417
        payer_name=None,
418
        bank_notification=None
419
    ) -> 'Transaction':
420

421 4
        if isinstance(bank_notification, str):
422 4
            text = bank_notification
423
        else:
424 4
            text = ''
425

426 4
        number = self._next_transaction_number
427 4
        self._next_transaction_number += 1
428

429 4
        transaction = PaymentRequest(
430
            service_code=self.service_code,
431
            type=transaction_type,
432
            number=number,
433
            date=due_date,
434
            amount=amount,
435
            kid=kid,
436
            reference=reference,
437
            text=text,
438
            payer_name=payer_name,
439
        )
440 4
        self.transactions.append(transaction)
441 4
        return transaction
442

443 4
    def get_num_transactions(self) -> int:
444
        """Get number of transactions in the assignment."""
445 4
        return len(self.transactions)
446

447 4
    def get_num_records(self) -> int:
448
        """Get number of records in the assignment.
449

450
        Includes the assignment's start and end record.
451
        """
452

453 4
        return 2 + sum(
454
            len(list(transaction.to_records()))
455
            for transaction in self.transactions
456
        )
457

458 4
    def get_total_amount(self) -> Decimal:
459
        """Get the total amount from all transactions in the assignment."""
460 4
        transactions = [
461
            transaction
462
            for transaction in self.transactions
463
            if hasattr(transaction, 'amount')
464
        ]
465 4
        if not transactions:
466 4
            return Decimal(0)
467 4
        return sum(transaction.amount for transaction in transactions)
468

469 4
    def get_earliest_transaction_date(self) -> Optional[datetime.date]:
470
        """Get earliest date from the assignment's transactions."""
471 4
        transactions = [
472
            transaction
473
            for transaction in self.transactions
474
            if hasattr(transaction, 'date')
475
        ]
476 4
        if not transactions:
477 4
            return None
478 4
        return min(transaction.date for transaction in transactions)
479

480 4
    def get_latest_transaction_date(self) -> Optional[datetime.date]:
481
        """Get latest date from the assignment's transactions."""
482 4
        transactions = [
483
            transaction
484
            for transaction in self.transactions
485
            if hasattr(transaction, 'date')
486
        ]
487 4
        if not transactions:
488 4
            return None
489 4
        return max(transaction.date for transaction in transactions)
490

491

492 4
@attr.s
493 1
class Agreement:
494
    """Agreement contains an AvtaleGiro agreement update.
495

496
    Agreements are only found in assignments of the
497
    :attr:`~netsgiro.AssignmentType.AVTALEGIRO_AGREEMENTS` type, which are only
498
    created by Nets.
499
    """
500

501
    #: The service code. One of :class:`~netsgiro.ServiceCode`.
502 4
    service_code = attr.ib(converter=netsgiro.ServiceCode)
503

504
    #: Transaction number. Unique and ordered within an assignment.
505 4
    number = attr.ib(validator=instance_of(int))
506

507
    #: Type of agreement registration update.
508
    #: One of :class:`~netsgiro.AvtaleGiroRegistrationType`.
509 4
    registration_type = attr.ib(converter=netsgiro.AvtaleGiroRegistrationType)
510

511
    #: KID number to identify the customer and invoice.
512 4
    kid = attr.ib(validator=optional(instance_of(str)))
513

514
    #: Whether the payer wants notification about payment requests.
515 4
    notify = attr.ib(validator=instance_of(bool))
516

517 4
    TRANSACTION_TYPE = netsgiro.TransactionType.AVTALEGIRO_AGREEMENT
518

519 4
    @classmethod
520 4
    def from_records(cls, records: List[Record]) -> 'Agreement':
521
        """Build an Agreement object from a list of record objects."""
522

523 4
        assert len(records) == 1
524 4
        record = records[0]
525 4
        assert isinstance(record, netsgiro.records.AvtaleGiroAgreement)
526 4
        assert (
527
            record.transaction_type
528
            == netsgiro.TransactionType.AVTALEGIRO_AGREEMENT
529
        )
530

531 4
        return cls(
532
            service_code=record.service_code,
533
            number=record.transaction_number,
534
            registration_type=record.registration_type,
535
            kid=record.kid,
536
            notify=record.notify,
537
        )
538

539 4
    def to_records(self) -> Iterable[Record]:
540
        """Convert the agreement to a list of records."""
541 4
        yield netsgiro.records.AvtaleGiroAgreement(
542
            service_code=self.service_code,
543
            transaction_type=self.TRANSACTION_TYPE,
544
            transaction_number=self.number,
545
            registration_type=self.registration_type,
546
            kid=self.kid,
547
            notify=self.notify,
548
        )
549

550

551 4
@attr.s
552 1
class PaymentRequest:
553
    """PaymentRequest contains an AvtaleGiro payment request or cancellation.
554

555
    To create a transaction, you will normally use the helper methods on
556
    :class:`~netsgiro.Assignment`:
557
    :meth:`~netsgiro.Assignment.add_payment_request` and
558
    :meth:`~netsgiro.Assignment.add_payment_cancellation`.
559
    """
560

561
    #: The service code. One of :class:`~netsgiro.ServiceCode`.
562 4
    service_code = attr.ib(converter=netsgiro.ServiceCode)
563

564
    #: The transaction type. One of :class:`~netsgiro.TransactionType`.
565 4
    type = attr.ib(converter=netsgiro.TransactionType)
566

567
    #: Transaction number. Unique and ordered within an assignment.
568 4
    number = attr.ib(validator=instance_of(int))
569

570
    #: The due date.
571 4
    date = attr.ib(validator=instance_of(datetime.date))
572

573
    #: Transaction amount in NOK with two decimals.
574 4
    amount = attr.ib(converter=Decimal)
575

576
    #: KID number to identify the customer and invoice.
577 4
    kid = attr.ib(validator=optional(instance_of(str)))
578

579
    #: This is a specification line that will, if set, be displayed on the
580
    #: payers account statement. Alphanumeric, max 25 chars.
581 4
    reference = attr.ib(validator=optional(instance_of(str)))
582

583
    #: This is up to 42 lines of 80 chars each of free text used by the bank to
584
    #: notify the payer about the payment request. It is not used if the payee
585
    #: is responsible for notifying the payer.
586 4
    text = attr.ib(validator=optional(instance_of(str)))
587

588
    #: The value is only used to help the payee cross-reference reports from
589
    #: Nets with their own records. It is not visible to the payer.
590 4
    payer_name = attr.ib(validator=optional(instance_of(str)))
591

592 4
    @property
593 4
    def amount_in_cents(self) -> int:
594
        """Transaction amount in NOK cents."""
595 4
        return int(self.amount * 100)
596

597 4
    @classmethod
598 4
    def from_records(cls, records: List[Record]) -> 'Transaction':
599
        """Build a Transaction object from a list of record objects."""
600 4
        amount_item_1 = records.pop(0)
601 4
        assert isinstance(
602
            amount_item_1, netsgiro.records.TransactionAmountItem1
603
        )
604 4
        amount_item_2 = records.pop(0)
605 4
        assert isinstance(
606
            amount_item_2, netsgiro.records.TransactionAmountItem2
607
        )
608

609 4
        text = netsgiro.records.TransactionSpecification.to_text(records)
610

611 4
        return cls(
612
            service_code=amount_item_1.service_code,
613
            type=amount_item_1.transaction_type,
614
            number=amount_item_1.transaction_number,
615
            date=amount_item_1.nets_date,
616
            amount=Decimal(amount_item_1.amount) / 100,
617
            kid=amount_item_1.kid,
618
            reference=amount_item_2.reference,
619
            text=text,
620
            payer_name=amount_item_2.payer_name,
621
        )
622

623 4
    def to_records(self) -> Iterable[Record]:
624
        """Convert the transaction to a list of records."""
625 4
        yield netsgiro.records.TransactionAmountItem1(
626
            service_code=self.service_code,
627
            transaction_type=self.type,
628
            transaction_number=self.number,
629
            nets_date=self.date,
630
            amount=self.amount_in_cents,
631
            kid=self.kid,
632
        )
633 4
        yield netsgiro.records.TransactionAmountItem2(
634
            service_code=self.service_code,
635
            transaction_type=self.type,
636
            transaction_number=self.number,
637
            reference=self.reference,
638
            payer_name=self.payer_name,
639
        )
640

641 4
        if self.type == (
642
            netsgiro.TransactionType.AVTALEGIRO_WITH_BANK_NOTIFICATION
643
        ):
644 4
            yield from netsgiro.records.TransactionSpecification.from_text(
645
                service_code=self.service_code,
646
                transaction_type=self.type,
647
                transaction_number=self.number,
648
                text=self.text,
649
            )
650

651

652 4
@attr.s
653 1
class Transaction:
654
    """Transaction contains an OCR Giro transaction.
655

656
    Transactions are found in assignments with the service code
657
    :attr:`~netsgiro.ServiceCode.OCR_GIRO` type, which are only
658
    created by Nets.
659
    """
660

661
    #: The service code. One of :class:`~netsgiro.ServiceCode`.
662 4
    service_code = attr.ib(converter=netsgiro.ServiceCode)
663

664
    #: The transaction type. One of :class:`~netsgiro.TransactionType`.
665 4
    type = attr.ib(converter=netsgiro.TransactionType)
666

667
    #: Transaction number. Unique and ordered within an assignment.
668 4
    number = attr.ib(validator=instance_of(int))
669

670
    #: Nets' processing date.
671 4
    date = attr.ib(validator=instance_of(datetime.date))
672

673
    #: Transaction amount in NOK with two decimals.
674 4
    amount = attr.ib(converter=Decimal)
675

676
    #: KID number to identify the customer and invoice.
677 4
    kid = attr.ib(validator=optional(instance_of(str)))
678

679
    #: The value depends on the payment method.
680 4
    reference = attr.ib(validator=optional(instance_of(str)))
681

682
    #: Up to 40 chars of free text from the payment terminal.
683 4
    text = attr.ib(validator=optional(instance_of(str)))
684

685
    #: Used for OCR Giro.
686 4
    centre_id = attr.ib(validator=optional(str_of_length(2)))
687

688
    #: Used for OCR Giro.
689 4
    day_code = attr.ib(validator=optional(instance_of(int)))
690

691
    #: Used for OCR Giro.
692 4
    partial_settlement_number = attr.ib(validator=optional(instance_of(int)))
693

694
    #: Used for OCR Giro.
695 4
    partial_settlement_serial_number = attr.ib(
696
        validator=optional(str_of_length(5))
697
    )
698

699
    #: Used for OCR Giro.
700 4
    sign = attr.ib(validator=optional(str_of_length(1)))
701

702
    #: Used for OCR Giro.
703 4
    form_number = attr.ib(validator=optional(str_of_length(10)))
704

705
    #: Used for OCR Giro.
706 4
    bank_date = attr.ib(validator=optional(instance_of(datetime.date)))
707

708
    #: Used for OCR Giro.
709 4
    debit_account = attr.ib(validator=optional(str_of_length(11)))
710

711 4
    _filler = attr.ib(validator=optional(str_of_length(7)))
712

713 4
    @property
714 4
    def amount_in_cents(self) -> int:
715
        """Transaction amount in NOK cents."""
716 4
        return int(self.amount * 100)
717

718 4
    @classmethod
719 4
    def from_records(cls, records: List[Record]) -> 'Transaction':
720
        """Build a Transaction object from a list of record objects."""
721 4
        amount_item_1 = records.pop(0)
722 4
        assert isinstance(
723
            amount_item_1, netsgiro.records.TransactionAmountItem1
724
        )
725 4
        amount_item_2 = records.pop(0)
726 4
        assert isinstance(
727
            amount_item_2, netsgiro.records.TransactionAmountItem2
728
        )
729

730 4
        if len(records) == 1 and isinstance(
731
            records[0], netsgiro.records.TransactionAmountItem3
732
        ):
733 4
            text = records[0].text
734
        else:
735 4
            text = None
736

737 4
        return cls(
738
            service_code=amount_item_1.service_code,
739
            type=amount_item_1.transaction_type,
740
            number=amount_item_1.transaction_number,
741
            date=amount_item_1.nets_date,
742
            amount=Decimal(amount_item_1.amount) / 100,
743
            kid=amount_item_1.kid,
744
            reference=amount_item_2.reference,
745
            text=text,
746
            centre_id=amount_item_1.centre_id,
747
            day_code=amount_item_1.day_code,
748
            partial_settlement_number=amount_item_1.partial_settlement_number,
749
            partial_settlement_serial_number=(
750
                amount_item_1.partial_settlement_serial_number
751
            ),
752
            sign=amount_item_1.sign,
753
            form_number=amount_item_2.form_number,
754
            bank_date=amount_item_2.bank_date,
755
            debit_account=amount_item_2.debit_account,
756
            filler=amount_item_2._filler,
757
        )
758

759 4
    def to_records(self) -> Iterable[Record]:
760
        """Convert the transaction to a list of records."""
761 4
        yield netsgiro.records.TransactionAmountItem1(
762
            service_code=self.service_code,
763
            transaction_type=self.type,
764
            transaction_number=self.number,
765
            nets_date=self.date,
766
            amount=self.amount_in_cents,
767
            kid=self.kid,
768
            centre_id=self.centre_id,
769
            day_code=self.day_code,
770
            partial_settlement_number=self.partial_settlement_number,
771
            partial_settlement_serial_number=(
772
                self.partial_settlement_serial_number
773
            ),
774
            sign=self.sign,
775
        )
776 4
        yield netsgiro.records.TransactionAmountItem2(
777
            service_code=self.service_code,
778
            transaction_type=self.type,
779
            transaction_number=self.number,
780
            reference=self.reference,
781
            form_number=self.form_number,
782
            bank_date=self.bank_date,
783
            debit_account=self.debit_account,
784
            filler=self._filler,
785
        )
786

787 4
        if self.type in (
788
            netsgiro.TransactionType.REVERSING_WITH_TEXT,
789
            netsgiro.TransactionType.PURCHASE_WITH_TEXT,
790
        ):
791 4
            yield netsgiro.records.TransactionAmountItem3(
792
                service_code=self.service_code,
793
                transaction_type=self.type,
794
                transaction_number=self.number,
795
                text=self.text,
796
            )
797

798

799 4
def parse(data: str) -> Transmission:
800
    """Parse an OCR file into a Transmission object."""
801 4
    records = netsgiro.records.parse(data)
802 4
    return Transmission.from_records(records)

Read our documentation on viewing source code .

Loading