Completed
Push — main ( 4a33dd...b7acb4 )
by Jochen
03:55
created

  A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
nop 1
ccs 2
cts 2
cp 1
crap 1
1
"""
2
byceps.services.shop.sequence.service
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2006-2020 Jochen Kupperschmidt
6
:License: Modified BSD, see LICENSE for details.
7
"""
8
9 1
from typing import List, Optional
10
11 1
from ....database import db
12
13 1
from ..article.transfer.models import ArticleNumber
14 1
from ..order.transfer.models import OrderNumber
15
16 1
from ..shop.transfer.models import ShopID
17
18 1
from .models import NumberSequence as DbNumberSequence
19 1
from .transfer.models import NumberSequence, NumberSequenceID, Purpose
20
21
22 1
def create_sequence(
23
    shop_id: ShopID,
24
    purpose: Purpose,
25
    prefix: str,
26
    *,
27
    value: Optional[int] = None,
28
) -> NumberSequenceID:
29
    """Create a sequence for that shop and purpose."""
30 1
    sequence = DbNumberSequence(shop_id, purpose, prefix, value=value)
31
32 1
    db.session.add(sequence)
33 1
    db.session.commit()
34
35 1
    return sequence.id
36
37
38 1
def create_article_number_sequence(
39
    shop_id: ShopID, prefix: str, *, value: Optional[int] = None
40
) -> NumberSequenceID:
41 1
    return create_sequence(shop_id, Purpose.article, prefix, value=value)
42
43
44 1
def create_order_number_sequence(
45
    shop_id: ShopID, prefix: str, *, value: Optional[int] = None
46
) -> NumberSequenceID:
47 1
    return create_sequence(shop_id, Purpose.order, prefix, value=value)
48
49
50 1
def delete_sequence(sequence_id: NumberSequenceID) -> None:
51
    """Delete the sequence."""
52 1
    db.session.query(DbNumberSequence) \
53
        .filter_by(id=sequence_id) \
54
        .delete()
55
56 1
    db.session.commit()
57
58
59 1
def delete_article_number_sequence(sequence_id: NumberSequenceID) -> None:
60
    """Delete the article sequence."""
61 1
    delete_sequence(sequence_id)
62
63
64 1
def delete_order_number_sequence(sequence_id: NumberSequenceID) -> None:
65
    """Delete the order sequence."""
66 1
    delete_sequence(sequence_id)
67
68
69 1
def find_article_number_sequence(
70
    sequence_id: NumberSequenceID,
71
) -> Optional[NumberSequence]:
72
    """Return the article number sequence, or `None` if the sequence ID
73
    is unknown or if the sequence's purpose is not article numbers.
74
    """
75
    return _find_sequence(sequence_id, Purpose.article)
76
77
78 1
def find_order_number_sequence(
79
    sequence_id: NumberSequenceID,
80
) -> Optional[NumberSequence]:
81
    """Return the order number sequence, or `None` if the sequence ID
82
    is unknown or if the sequence's purpose is not order numbers.
83
    """
84 1
    return _find_sequence(sequence_id, Purpose.order)
85
86
87 1
def _find_sequence(
88
    sequence_id: NumberSequenceID, purpose: Purpose
89
) -> Optional[NumberSequence]:
90
    """Return the number sequence, or `None` if the sequence ID is
91
    unknown or if the sequence's purpose is not the given one.
92
    """
93 1
    return DbNumberSequence.query \
94
        .filter_by(id=sequence_id) \
95
        .filter_by(_purpose=purpose.name) \
96
        .one_or_none()
97
98
99 1
class NumberGenerationFailed(Exception):
100
    """Indicate that generating a prefixed, sequential number has failed."""
101
102 1
    def __init__(self, message: str) -> None:
103
        self.message = message
104
105
106 1
def generate_article_number(sequence_id: NumberSequenceID) -> ArticleNumber:
107
    """Generate and reserve an unused, unique article number from this
108
    sequence.
109
    """
110 1
    sequence = _get_next_sequence_step(sequence_id, Purpose.article)
111 1
    return ArticleNumber(f'{sequence.prefix}{sequence.value:05d}')
112
113
114 1
def generate_order_number(sequence_id: NumberSequenceID) -> OrderNumber:
115
    """Generate and reserve an unused, unique order number from this
116
    sequence.
117
    """
118 1
    sequence = _get_next_sequence_step(sequence_id, Purpose.order)
119 1
    return OrderNumber(f'{sequence.prefix}{sequence.value:05d}')
120
121
122 1
def _get_next_sequence_step(
123
    sequence_id: NumberSequenceID, purpose: Purpose
124
) -> NumberSequence:
125
    """Calculate and reserve the next number from this sequence.
126
127
    The purpose must be specified to prevent using a sequence of the
128
    wrong purpose, even though the sequence ID is unique.
129
    """
130 1
    sequence = DbNumberSequence.query \
131
        .filter_by(id=sequence_id) \
132
        .filter_by(_purpose=purpose.name) \
133
        .with_for_update() \
134
        .one_or_none()
135
136 1
    if sequence is None:
137
        raise NumberGenerationFailed(
138
            f'No sequence found for ID "{sequence_id}" '
139
            f'and purpose "{purpose.name}".'
140
        )
141
142 1
    sequence.value = DbNumberSequence.value + 1
143 1
    db.session.commit()
144
145 1
    return sequence
146
147
148 1
def find_article_number_sequences_for_shop(
149
    shop_id: ShopID,
150
) -> List[NumberSequence]:
151
    """Return the article number sequences defined for that shop."""
152
    return _find_number_sequences(shop_id, Purpose.article)
153
154
155 1
def find_order_number_sequences_for_shop(
156
    shop_id: ShopID,
157
) -> List[NumberSequence]:
158
    """Return the order number sequences defined for that shop."""
159
    return _find_number_sequences(shop_id, Purpose.order)
160
161
162 1
def _find_number_sequences(
163
    shop_id: ShopID, purpose: Purpose
164
) -> List[NumberSequence]:
165
    sequences = DbNumberSequence.query \
166
        .filter_by(shop_id=shop_id) \
167
        .filter_by(_purpose=purpose.name) \
168
        .all()
169
170
    return [_db_entity_to_number_sequence(sequence) for sequence in sequences]
171
172
173 1
def _db_entity_to_number_sequence(entity: DbNumberSequence) -> NumberSequence:
174
    return NumberSequence(
175
        entity.id,
176
        entity.shop_id,
177
        entity.purpose,
178
        entity.prefix,
179
        entity.value,
180
    )
181