python-redsys 1.2.2__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
redsys/api/sca.py ADDED
@@ -0,0 +1,23 @@
1
+ """PSD2 strong customer authentication (SCA) exemption codes for Ds_Merchant_Excep_SCA (REST guide 4.0.1)."""
2
+
3
+ LWV = 'LWV'
4
+ """Low-value transaction: up to 30 EUR, with issuer-side counters (5 operations or 100 EUR accumulated)."""
5
+
6
+ TRA = 'TRA'
7
+ """Transaction risk analysis performed by the acquirer or the merchant."""
8
+
9
+ MIT = 'MIT'
10
+ """Merchant-initiated transaction, no cardholder interaction (stored credentials, subscriptions).
11
+
12
+ Sent together with direct_payment "true"."""
13
+
14
+ COR = 'COR'
15
+ """Secure corporate payment, business-to-business only."""
16
+
17
+ ATD = 'ATD'
18
+ """Delegated authentication, a card-scheme program."""
19
+
20
+ EXEMPTIONS = (LWV, TRA, MIT, COR, ATD)
21
+
22
+ QUERY = 'Y'
23
+ """Sent on the 3DS initialization to ask which exemptions the card allows; the answer comes back in Ds_Excep_SCA."""
@@ -0,0 +1,74 @@
1
+ AUTHORIZATION = 0
2
+ """Authorization."""
3
+
4
+ PREAUTHORIZATION = 1
5
+ """Pre-authorization."""
6
+
7
+ CONFIRMATION = 2
8
+ """Confirmation."""
9
+
10
+ REFUND = 3
11
+ """Refund."""
12
+
13
+ AUTHENTICATION = 7
14
+ """Authentication."""
15
+
16
+ AUTHENTICATION_CONFIRMATION = 8
17
+ """Authentication confirmation"""
18
+
19
+ CANCELLATION = 9
20
+ """Cancellation"""
21
+
22
+ REPLACEMENT_PREAUTHORIZATION = 11
23
+ """Replacement preauthorization: extends an existing preauthorization."""
24
+
25
+ PAY_GOLD = 15
26
+ """Pay gold"""
27
+
28
+ AUTHENTICATION_PUCE = 17
29
+ """PUCE authentication"""
30
+
31
+ REFUND_WITHOUT_ORIGINAL = 34
32
+ """Refund without original"""
33
+
34
+ BETTING_PRICE = 37
35
+ """Betting price"""
36
+
37
+ PAY_GOLD_AUTHORIZATION = 38
38
+ """PayGold authorization: the cardholder completes a PayGold request (type 15); sent by RedSys, not the merchant."""
39
+
40
+ DELETE_REFERENCE = 44
41
+ """Delete a stored card reference (token)."""
42
+
43
+ PAYMENT_CANCELLATION = 45
44
+ """Payment cancellation, for merchant time-outs or technical errors."""
45
+
46
+ REFUND_CANCELLATION = 46
47
+ """Refund cancellation, within 48 hours of the refund."""
48
+
49
+ AUTHENTICATION_CONFIRMATION_CANCELLATION = 47
50
+ """Cancellation of an authentication confirmation (type 8)."""
51
+
52
+ PARTIAL_CONFIRMATION = 48
53
+ """Partial confirmation of a preauthorization."""
54
+
55
+ TRANSACTION_TYPES = (
56
+ AUTHORIZATION,
57
+ PREAUTHORIZATION,
58
+ CONFIRMATION,
59
+ REFUND,
60
+ AUTHENTICATION,
61
+ AUTHENTICATION_CONFIRMATION,
62
+ CANCELLATION,
63
+ REPLACEMENT_PREAUTHORIZATION,
64
+ PAY_GOLD,
65
+ AUTHENTICATION_PUCE,
66
+ REFUND_WITHOUT_ORIGINAL,
67
+ BETTING_PRICE,
68
+ PAY_GOLD_AUTHORIZATION,
69
+ DELETE_REFERENCE,
70
+ PAYMENT_CANCELLATION,
71
+ REFUND_CANCELLATION,
72
+ AUTHENTICATION_CONFIRMATION_CANCELLATION,
73
+ PARTIAL_CONFIRMATION,
74
+ )
redsys/clients.py ADDED
@@ -0,0 +1,277 @@
1
+ """Client entry points for the RedSys platform.
2
+
3
+ The client is the single entry point: it holds the credentials and account
4
+ identity (secret key, merchant code, terminal, signature version) and acts as
5
+ the factory for every operation. You never build or import a request model --
6
+ call an operation method (``payment``, ``refund``, ...) or the low-level
7
+ ``request()`` they all delegate to, and you get back a ``Request``:
8
+
9
+ client = DirectClient(secret_key='...', merchant_code='...', terminal=1, sandbox=True)
10
+ response = client.payment(order='000000001', amount=Decimal('10.56'), currency=978).send()
11
+
12
+ Redirect requests end in ``.to_dict()`` (the browser submits them); direct
13
+ requests end in ``.send()`` (the library performs the REST call). Both verify
14
+ and parse incoming results through ``client.parse_response(payload)``.
15
+ """
16
+
17
+ from hmac import compare_digest
18
+ from logging import getLogger
19
+ from re import sub
20
+
21
+ from redsys.api import transactions
22
+ from redsys.crypto import (
23
+ DEFAULT_SIGNATURE_VERSION,
24
+ HMAC_SHA256_V1,
25
+ HMAC_SHA512_V1,
26
+ HMAC_SHA512_V2,
27
+ SIGNATURE_VERSIONS,
28
+ get_signer,
29
+ )
30
+ from redsys.exceptions import ResponseError
31
+ from redsys.models import (
32
+ CancellationMessage,
33
+ ConfirmationMessage,
34
+ DirectPaymentMessage,
35
+ DirectPreauthorizationMessage,
36
+ RedirectPaymentMessage,
37
+ RedirectPreauthorizationMessage,
38
+ RefundMessage,
39
+ Response,
40
+ )
41
+ from redsys.request import (
42
+ MERCHANT_PARAMETERS,
43
+ SIGNATURE,
44
+ SIGNATURE_VERSION,
45
+ Request,
46
+ )
47
+
48
+ logger = getLogger(__name__)
49
+
50
+ # Default HTTP timeout (seconds) for direct-connection network calls; override per call.
51
+ DEFAULT_TIMEOUT = 30
52
+
53
+ __all__ = [
54
+ 'Client',
55
+ 'RedirectClient',
56
+ 'DirectClient',
57
+ 'AsyncDirectClient',
58
+ 'Request',
59
+ 'SIGNATURE_VERSION',
60
+ 'MERCHANT_PARAMETERS',
61
+ 'SIGNATURE',
62
+ 'HMAC_SHA256_V1',
63
+ 'HMAC_SHA512_V1',
64
+ 'HMAC_SHA512_V2',
65
+ 'SIGNATURE_VERSIONS',
66
+ 'DEFAULT_SIGNATURE_VERSION',
67
+ ]
68
+
69
+
70
+ def _normalize_signature(signature) -> str:
71
+ """Reduce a signature to its alphanumeric core for comparison.
72
+
73
+ RedSys returns response signatures in URL-safe Base64 ('-', '_') while
74
+ standard Base64 uses ('+', '/'). Comparing only the alphanumeric characters
75
+ makes the two alphabets and any padding compare equal.
76
+ """
77
+ if isinstance(signature, bytes):
78
+ signature = signature.decode('utf-8')
79
+ return sub(r'[^a-zA-Z0-9]', '', signature)
80
+
81
+
82
+ class Client:
83
+ """Base client: credentials, account identity, request factory, response parsing."""
84
+
85
+ REAL_ENDPOINT: str | None = None
86
+ TEST_ENDPOINT: str | None = None
87
+
88
+ #: transaction_type -> request model. Unmapped types fall back to _DEFAULT_MODEL.
89
+ _DEFAULT_MODEL: type | None = None
90
+ _MODELS: dict[int, type] = {}
91
+
92
+ def __init__(
93
+ self,
94
+ secret_key: str,
95
+ *,
96
+ merchant_code: str | None = None,
97
+ terminal: int | None = None,
98
+ sandbox: bool = False,
99
+ signature_version: str = DEFAULT_SIGNATURE_VERSION,
100
+ ):
101
+ self._secret_key = secret_key
102
+ self.merchant_code = merchant_code
103
+ self.terminal = terminal
104
+ self.sandbox = sandbox
105
+ self.signature_version = signature_version
106
+ self.endpoint = self.TEST_ENDPOINT if sandbox else self.REAL_ENDPOINT
107
+
108
+ # -- request building -------------------------------------------------
109
+ def _inject(self, fields: dict) -> dict:
110
+ """Merge the client's account identity into a call's fields (call wins)."""
111
+ data: dict = {}
112
+ if self.merchant_code is not None:
113
+ data['merchant_code'] = self.merchant_code
114
+ if self.terminal is not None:
115
+ data['terminal'] = self.terminal
116
+ data.update(fields)
117
+ return data
118
+
119
+ def _model_for(self, transaction_type: int) -> type:
120
+ model = self._MODELS.get(transaction_type, self._DEFAULT_MODEL)
121
+ if model is None:
122
+ raise NotImplementedError('This client defines no request model; use a concrete subclass.')
123
+ return model
124
+
125
+ def request(self, transaction_type: int, **fields) -> Request:
126
+ """Build, validate and sign any operation by its transaction type.
127
+
128
+ The single funnel every named operation delegates to. Use it directly
129
+ for transaction types without a dedicated method (e.g. ``PAY_GOLD``).
130
+ """
131
+ data = self._inject(fields)
132
+ data.setdefault('transaction_type', transaction_type)
133
+ model = self._model_for(transaction_type)(**data)
134
+ return self._prepare(model)
135
+
136
+ def _prepare(self, model) -> Request:
137
+ return self._build_request(model)
138
+
139
+ def _build_request(self, model, signature_version: str | None = None, sender=None) -> Request:
140
+ version = signature_version or self.signature_version
141
+ signer = get_signer(self._secret_key, version)
142
+ merchant_parameters = signer.encode_parameters(model.prepare_parameters())
143
+ signature = signer.sign(model.get_order(), merchant_parameters)
144
+ return Request(merchant_parameters, signature, version, sender=sender)
145
+
146
+ # -- response ---------------------------------------------------------
147
+ def parse_response(self, payload: dict) -> Response:
148
+ """Verify the signature and parse a received payload into a ``Response``.
149
+
150
+ Accepts both the redirect callback form fields and the direct REST JSON
151
+ body. Raises ``ValueError`` if the signature does not match (the payload
152
+ is not from RedSys or was tampered with), or ``ResponseError`` if RedSys
153
+ answered with an error code instead of a signed result.
154
+ """
155
+ if MERCHANT_PARAMETERS not in payload:
156
+ error_code = payload.get('errorCode') or payload.get('Ds_ErrorCode')
157
+ raise ResponseError(error_code, payload)
158
+ merchant_parameters = payload[MERCHANT_PARAMETERS]
159
+ if isinstance(merchant_parameters, str):
160
+ merchant_parameters = merchant_parameters.encode('utf-8')
161
+ version = payload.get(SIGNATURE_VERSION, DEFAULT_SIGNATURE_VERSION)
162
+ response = Response.from_merchant_parameters(merchant_parameters)
163
+ # A signed result always carries Ds_Order; '' can never match, so a
164
+ # missing order simply fails verification below.
165
+ expected = get_signer(self._secret_key, version).sign(response.get_order() or '', merchant_parameters)
166
+ # Constant-time comparison to avoid leaking the signature via timing.
167
+ if not compare_digest(_normalize_signature(payload[SIGNATURE]), _normalize_signature(expected)):
168
+ raise ValueError('Signature is not valid.')
169
+ return response
170
+
171
+
172
+ class RedirectClient(Client):
173
+ """Redirect connection (secure method): builds the signed POST parameters the
174
+ browser submits to RedSys. The library performs no HTTP and no redirect."""
175
+
176
+ # Endpoints per the redirection guide 4.2 (RS.TE.CEL.MAN.0039).
177
+ REAL_ENDPOINT = 'https://sis.redsys.es/sis/realizarPago'
178
+ TEST_ENDPOINT = 'https://sis-t.redsys.es:25443/sis/realizarPago'
179
+ _DEFAULT_MODEL = RedirectPaymentMessage
180
+ _MODELS = {transactions.PREAUTHORIZATION: RedirectPreauthorizationMessage}
181
+
182
+ def payment(self, **fields) -> Request:
183
+ return self.request(transactions.AUTHORIZATION, **fields)
184
+
185
+ def preauthorization(self, **fields) -> Request:
186
+ return self.request(transactions.PREAUTHORIZATION, **fields)
187
+
188
+
189
+ def _require_httpx():
190
+ try:
191
+ import httpx
192
+ except ImportError as exc:
193
+ raise ImportError('Sending requires httpx. Install with: pip install "python-redsys[http]".') from exc
194
+ return httpx
195
+
196
+
197
+ class _DirectBase(Client):
198
+ """Shared REST direct-connection logic (transport-agnostic)."""
199
+
200
+ # Endpoints per the REST guide 4.0.1 (RS.TE.CEL.MAN.0037).
201
+ REAL_ENDPOINT = 'https://sis.redsys.es/sis/rest/trataPeticionREST'
202
+ TEST_ENDPOINT = 'https://sis-t.redsys.es:25443/sis/rest/trataPeticionREST'
203
+ INICIA_REAL_ENDPOINT = 'https://sis.redsys.es/sis/rest/iniciaPeticionREST'
204
+ INICIA_TEST_ENDPOINT = 'https://sis-t.redsys.es:25443/sis/rest/iniciaPeticionREST'
205
+
206
+ _DEFAULT_MODEL = DirectPaymentMessage
207
+ _MODELS = {
208
+ transactions.PREAUTHORIZATION: DirectPreauthorizationMessage,
209
+ transactions.CONFIRMATION: ConfirmationMessage,
210
+ transactions.REFUND: RefundMessage,
211
+ transactions.CANCELLATION: CancellationMessage,
212
+ transactions.REPLACEMENT_PREAUTHORIZATION: DirectPreauthorizationMessage,
213
+ transactions.PARTIAL_CONFIRMATION: ConfirmationMessage,
214
+ transactions.PAYMENT_CANCELLATION: CancellationMessage,
215
+ transactions.REFUND_CANCELLATION: CancellationMessage,
216
+ transactions.AUTHENTICATION_CONFIRMATION_CANCELLATION: CancellationMessage,
217
+ }
218
+
219
+ @property
220
+ def inicia_endpoint(self) -> str:
221
+ return self.INICIA_TEST_ENDPOINT if self.sandbox else self.INICIA_REAL_ENDPOINT
222
+
223
+ def payment(self, **fields) -> Request:
224
+ return self.request(transactions.AUTHORIZATION, **fields)
225
+
226
+ def preauthorization(self, **fields) -> Request:
227
+ return self.request(transactions.PREAUTHORIZATION, **fields)
228
+
229
+ def confirmation(self, **fields) -> Request:
230
+ return self.request(transactions.CONFIRMATION, **fields)
231
+
232
+ def refund(self, **fields) -> Request:
233
+ return self.request(transactions.REFUND, **fields)
234
+
235
+ def cancellation(self, **fields) -> Request:
236
+ return self.request(transactions.CANCELLATION, **fields)
237
+
238
+ def inicia(self, **fields) -> Request:
239
+ """3-D Secure step 1: build a request bound to the iniciaPeticionREST endpoint."""
240
+ model = self._DEFAULT_MODEL(**self._inject(fields))
241
+ return self._prepare(model, endpoint=self.inicia_endpoint)
242
+
243
+ def _prepare(self, model, endpoint: str | None = None) -> Request:
244
+ endpoint = endpoint or self.endpoint
245
+ return self._build_request(model, sender=self._make_sender(endpoint))
246
+
247
+ def _make_sender(self, endpoint):
248
+ raise NotImplementedError
249
+
250
+
251
+ class DirectClient(_DirectBase):
252
+ """Synchronous REST direct-connection client. Requires the httpx extra."""
253
+
254
+ def _make_sender(self, endpoint):
255
+ def sender(request, **httpx_kwargs):
256
+ httpx = _require_httpx()
257
+ httpx_kwargs.setdefault('timeout', DEFAULT_TIMEOUT)
258
+ http_response = httpx.post(endpoint, json=request.to_dict(), **httpx_kwargs)
259
+ http_response.raise_for_status()
260
+ return self.parse_response(http_response.json())
261
+
262
+ return sender
263
+
264
+
265
+ class AsyncDirectClient(_DirectBase):
266
+ """Asynchronous REST direct-connection client (httpx.AsyncClient). Requires the httpx extra."""
267
+
268
+ def _make_sender(self, endpoint):
269
+ async def sender(request, **httpx_kwargs):
270
+ httpx = _require_httpx()
271
+ httpx_kwargs.setdefault('timeout', DEFAULT_TIMEOUT)
272
+ async with httpx.AsyncClient() as client:
273
+ http_response = await client.post(endpoint, json=request.to_dict(), **httpx_kwargs)
274
+ http_response.raise_for_status()
275
+ return self.parse_response(http_response.json())
276
+
277
+ return sender
redsys/crypto.py ADDED
@@ -0,0 +1,123 @@
1
+ """Signature computation for the RedSys message envelope.
2
+
3
+ Every version derives a per-operation key by encrypting the order number under
4
+ the merchant secret, then HMACs the Base64 merchant parameters with that key.
5
+ They differ in cipher, padding, hash, and output encoding:
6
+
7
+ | Version | Cipher | Key | Order padding | HMAC | Signature encoding |
8
+ |------------------|---------|---------------------------|---------------|---------|-------------------------|
9
+ | HMAC_SHA256_V1 | 3DES | Base64-decoded secret | zero | SHA-256 | Base64 |
10
+ | HMAC_SHA512_V1 | AES-128 | first 16 chars of secret | PKCS7 | SHA-512 | Base64URL, '=' kept |
11
+ | HMAC_SHA512_V2 | AES-128 | first 16 chars of secret | PKCS7 | SHA-512 | Base64URL, '=' stripped |
12
+
13
+ HMAC_SHA512_V2 also sends Ds_MerchantParameters as unpadded Base64URL; the other
14
+ versions send standard Base64. Responses are decoded tolerantly in either form.
15
+
16
+ The SHA-512 versions key their HMAC with the Base64 text of the derived key and differ
17
+ only in the padding of the signature (RedSys PHP library 2.0.1, Signature.php);
18
+ HMAC_SHA256_V1 uses the raw derived bytes.
19
+
20
+ HMAC_SHA512_V1 is the REST guide 4.0.1 scheme; HMAC_SHA512_V2 (the default) is the one the
21
+ redirection guide 4.1 and later and RedSys's own libraries use; HMAC_SHA256_V1 is
22
+ still accepted (REST guide 4.0.1, Anexo 2). The cipher parameters are pinned by a
23
+ known-answer test against RedSys's official worked example (see tests/test_crypto.py).
24
+ """
25
+
26
+ from base64 import b64decode, b64encode, urlsafe_b64encode
27
+ from hashlib import sha256, sha512
28
+ from hmac import HMAC
29
+ from json import dumps
30
+
31
+ from cryptography.hazmat.decrepit.ciphers.algorithms import TripleDES # 3DES, in decrepit since cryptography 43
32
+ from cryptography.hazmat.primitives import padding
33
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
34
+
35
+ HMAC_SHA256_V1 = 'HMAC_SHA256_V1'
36
+ HMAC_SHA512_V1 = 'HMAC_SHA512_V1'
37
+ HMAC_SHA512_V2 = 'HMAC_SHA512_V2'
38
+
39
+
40
+ class Signer:
41
+ """Computes the signature for a single signature version."""
42
+
43
+ version: str
44
+
45
+ def __init__(self, secret_key: str):
46
+ self._secret_key = secret_key
47
+
48
+ def encode_parameters(self, parameters: dict) -> bytes:
49
+ """Encode the wire dict as the Ds_MerchantParameters string this version expects."""
50
+ return b64encode(dumps(parameters, sort_keys=True).encode('utf-8'))
51
+
52
+ def _derive_key(self, order: str) -> bytes:
53
+ raise NotImplementedError
54
+
55
+ def sign(self, order: str, merchant_parameters: bytes) -> bytes:
56
+ raise NotImplementedError
57
+
58
+
59
+ class Sha256V1Signer(Signer):
60
+ version = HMAC_SHA256_V1
61
+
62
+ def _derive_key(self, order: str) -> bytes:
63
+ key = b64decode(self._secret_key)
64
+ data = order.encode('utf-8')
65
+ data += b'\x00' * (-len(data) % 8) # zero-pad to the 3DES block
66
+ encryptor = Cipher(TripleDES(key), modes.CBC(b'\x00' * 8)).encryptor()
67
+ return encryptor.update(data) + encryptor.finalize()
68
+
69
+ def sign(self, order: str, merchant_parameters: bytes) -> bytes:
70
+ derived = self._derive_key(order)
71
+ return b64encode(HMAC(derived, merchant_parameters, sha256).digest())
72
+
73
+
74
+ class Sha512V1Signer(Signer):
75
+ version = HMAC_SHA512_V1
76
+
77
+ def _derive_key(self, order: str) -> bytes:
78
+ # Secret truncated to 16 chars, right-padded with '0' if shorter (REST manual v4.0.1).
79
+ key = self._secret_key[:16].ljust(16, '0').encode('utf-8')
80
+ padder = padding.PKCS7(128).padder() # AES block size in bits
81
+ data = padder.update(order.encode('utf-8')) + padder.finalize()
82
+ encryptor = Cipher(algorithms.AES(key), modes.CBC(b'\x00' * 16)).encryptor()
83
+ return encryptor.update(data) + encryptor.finalize()
84
+
85
+ def sign(self, order: str, merchant_parameters: bytes) -> bytes:
86
+ # The HMAC key is the Base64 text of the derived key, not its raw bytes
87
+ # (confirmed live against the RedSys test environment; raw bytes -> SIS0042).
88
+ derived = b64encode(self._derive_key(order))
89
+ # REST manual: Base64URL with the '=' padding kept.
90
+ return urlsafe_b64encode(HMAC(derived, merchant_parameters, sha512).digest())
91
+
92
+
93
+ class Sha512V2Signer(Sha512V1Signer):
94
+ version = HMAC_SHA512_V2
95
+
96
+ def encode_parameters(self, parameters: dict) -> bytes:
97
+ # V2 endpoints decode Ds_MerchantParameters as unpadded Base64URL: the 3DS
98
+ # initialization rejects standard or padded Base64 with SIS0430 (verified
99
+ # live 2026-10-25), and the RedSys PHP library sends it this way.
100
+ return urlsafe_b64encode(dumps(parameters, sort_keys=True).encode('utf-8')).rstrip(b'=')
101
+
102
+ def sign(self, order: str, merchant_parameters: bytes) -> bytes:
103
+ # Same derived key and HMAC as HMAC_SHA512_V1; only the '=' padding goes
104
+ # (RedSys PHP library 2.0.1, base64_url_encode_safe).
105
+ return super().sign(order, merchant_parameters).rstrip(b'=')
106
+
107
+
108
+ _SIGNERS = {
109
+ HMAC_SHA256_V1: Sha256V1Signer,
110
+ HMAC_SHA512_V1: Sha512V1Signer,
111
+ HMAC_SHA512_V2: Sha512V2Signer,
112
+ }
113
+
114
+ SIGNATURE_VERSIONS = tuple(_SIGNERS)
115
+ DEFAULT_SIGNATURE_VERSION = HMAC_SHA512_V2
116
+
117
+
118
+ def get_signer(secret_key: str, signature_version: str = DEFAULT_SIGNATURE_VERSION) -> Signer:
119
+ try:
120
+ signer_class = _SIGNERS[signature_version]
121
+ except KeyError:
122
+ raise ValueError(f'Unsupported signature version: {signature_version!r}') from None
123
+ return signer_class(secret_key)
redsys/exceptions.py ADDED
@@ -0,0 +1,19 @@
1
+ class RedsysError(Exception):
2
+ """Base class for redsys errors."""
3
+
4
+
5
+ class ResponseError(RedsysError):
6
+ """RedSys returned an error payload instead of a signed result."""
7
+
8
+ def __init__(self, error_code, payload=None):
9
+ self.error_code = error_code
10
+ self.payload = payload
11
+ super().__init__(f'RedSys returned error: {error_code}')
12
+
13
+
14
+ class ValidationError(RedsysError):
15
+ """A parameter failed validation."""
16
+
17
+
18
+ class UnknownParameterWarning(UserWarning):
19
+ """A request carries a RedSys wire-name parameter this library version does not model; it is sent as given."""