CprModel.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <?php
  2. namespace Models;
  3. class CprModel
  4. {
  5. private \PDO $pdo;
  6. private static ?array $columnsMeta = null;
  7. public function __construct()
  8. {
  9. if (isset($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof \PDO) {
  10. $this->pdo = $GLOBALS['pdo'];
  11. return;
  12. }
  13. throw new \RuntimeException('Global PDO connection not initialized');
  14. }
  15. /**
  16. * @return array<string, array{nullable: bool, data_type: string}>
  17. */
  18. private function getColumnsMeta(): array
  19. {
  20. if (self::$columnsMeta !== null) {
  21. return self::$columnsMeta;
  22. }
  23. $stmt = $this->pdo->prepare(
  24. 'SELECT column_name, is_nullable, data_type
  25. FROM information_schema.columns
  26. WHERE table_schema = current_schema()
  27. AND table_name = :table
  28. ORDER BY ordinal_position'
  29. );
  30. $stmt->execute(['table' => 'cpr']);
  31. $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
  32. if (!$rows) {
  33. throw new \RuntimeException('Unable to load CPR table metadata');
  34. }
  35. $meta = [];
  36. foreach ($rows as $row) {
  37. $meta[$row['column_name']] = [
  38. 'nullable' => strtoupper((string)$row['is_nullable']) === 'YES',
  39. 'data_type' => (string)$row['data_type'],
  40. ];
  41. }
  42. self::$columnsMeta = $meta;
  43. return self::$columnsMeta;
  44. }
  45. /**
  46. * @return array<string, array{nullable: bool, data_type: string}>
  47. */
  48. public function getUserColumns(): array
  49. {
  50. $meta = $this->getColumnsMeta();
  51. unset($meta['cpr_id']);
  52. return array_diff_key($meta, ['status_id' => true, 'payment_id' => true, 'user_id' => true, 'company_id' => true]);
  53. }
  54. public function create(array $data, int $statusId, int $paymentId, int $userId, int $companyId): array
  55. {
  56. $data = $this->flattenB3Arrays($data);
  57. $meta = $this->getColumnsMeta();
  58. $columns = [];
  59. $placeholders = [];
  60. $params = [];
  61. foreach ($meta as $column => $info) {
  62. if ($column === 'cpr_id') {
  63. continue;
  64. }
  65. if ($column === 'status_id') {
  66. $columns[] = '"status_id"';
  67. $placeholders[] = ':status_id';
  68. $params['status_id'] = $statusId;
  69. continue;
  70. }
  71. if ($column === 'payment_id') {
  72. $columns[] = '"payment_id"';
  73. $placeholders[] = ':payment_id';
  74. $params['payment_id'] = $paymentId;
  75. continue;
  76. }
  77. if ($column === 'user_id') {
  78. $columns[] = '"user_id"';
  79. $placeholders[] = ':user_id';
  80. $params['user_id'] = $userId;
  81. continue;
  82. }
  83. if ($column === 'company_id') {
  84. $columns[] = '"company_id"';
  85. $placeholders[] = ':company_id';
  86. $params['company_id'] = $companyId;
  87. continue;
  88. }
  89. if (!array_key_exists($column, $data)) {
  90. if ($column === 'cpr_ticker') {
  91. $columns[] = '"' . $column . '"';
  92. $placeholders[] = ':' . $column;
  93. $params[$column] = '';
  94. continue;
  95. }
  96. if ($info['nullable']) {
  97. $columns[] = '"' . $column . '"';
  98. $placeholders[] = ':' . $column;
  99. $params[$column] = null;
  100. continue;
  101. }
  102. throw new \InvalidArgumentException("Missing field: {$column}");
  103. }
  104. $value = $data[$column];
  105. if ($column === 'cpr_children_codes') {
  106. $value = $this->normalizeChildrenCodes($value);
  107. }
  108. if (in_array($column, $this->getSemicolonListColumns(), true)) {
  109. $value = $this->normalizeSemicolonList($value, $column);
  110. }
  111. $columns[] = '"' . $column . '"';
  112. $placeholders[] = ':' . $column;
  113. $params[$column] = $value;
  114. }
  115. $sql = 'INSERT INTO "cpr" (' . implode(', ', $columns) . ')
  116. VALUES (' . implode(', ', $placeholders) . ')
  117. RETURNING cpr_id';
  118. $stmt = $this->pdo->prepare($sql);
  119. $stmt->execute($params);
  120. $cprId = (int)$stmt->fetchColumn();
  121. $record = $this->fetchById($cprId);
  122. if (!$record) {
  123. throw new \RuntimeException('Failed to load created CPR record');
  124. }
  125. if (isset($record['cpr_children_codes'])) {
  126. $record['cpr_children_codes'] = $this->decodeChildrenCodes((string)$record['cpr_children_codes']);
  127. }
  128. $record['cpr_id'] = (int)$record['cpr_id'];
  129. if (isset($record['status_id'])) {
  130. $record['status_id'] = (int)$record['status_id'];
  131. }
  132. if (isset($record['payment_id'])) {
  133. $record['payment_id'] = (int)$record['payment_id'];
  134. }
  135. if (isset($record['user_id'])) {
  136. $record['user_id'] = (int)$record['user_id'];
  137. }
  138. if (isset($record['company_id'])) {
  139. $record['company_id'] = (int)$record['company_id'];
  140. }
  141. return $record;
  142. }
  143. public function updateTokenId(int $cprId, int $tokenId): void
  144. {
  145. $stmt = $this->pdo->prepare('UPDATE "cpr" SET token_id = :token_id WHERE cpr_id = :cpr_id');
  146. $stmt->execute([
  147. 'token_id' => $tokenId,
  148. 'cpr_id' => $cprId,
  149. ]);
  150. }
  151. public function updateCompanyId(int $cprId, int $companyId): void
  152. {
  153. if ($cprId <= 0) {
  154. throw new \InvalidArgumentException('Invalid CPR id provided');
  155. }
  156. if ($companyId <= 0) {
  157. throw new \InvalidArgumentException('Invalid company id provided');
  158. }
  159. $stmt = $this->pdo->prepare('UPDATE "cpr" SET company_id = :company_id WHERE cpr_id = :cpr_id');
  160. $stmt->execute([
  161. 'company_id' => $companyId,
  162. 'cpr_id' => $cprId,
  163. ]);
  164. if ($stmt->rowCount() === 0) {
  165. throw new \RuntimeException('CPR record not found for update');
  166. }
  167. }
  168. public function updateTicker(int $cprId, string $ticker): void
  169. {
  170. if ($cprId <= 0) {
  171. throw new \InvalidArgumentException('Invalid CPR id provided');
  172. }
  173. $normalizedTicker = trim($ticker);
  174. $stmt = $this->pdo->prepare('UPDATE "cpr" SET cpr_ticker = :cpr_ticker WHERE cpr_id = :cpr_id');
  175. $stmt->execute([
  176. 'cpr_ticker' => $normalizedTicker,
  177. 'cpr_id' => $cprId,
  178. ]);
  179. }
  180. private function normalizeChildrenCodes($value): string
  181. {
  182. if (is_array($value)) {
  183. $value = array_map('strval', array_values($value));
  184. if (!$value) {
  185. throw new \InvalidArgumentException('cpr_children_codes must not be empty');
  186. }
  187. $encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
  188. if ($encoded === false) {
  189. throw new \InvalidArgumentException('Invalid cpr_children_codes payload');
  190. }
  191. return $encoded;
  192. }
  193. if (is_string($value) && trim($value) !== '') {
  194. return $value;
  195. }
  196. throw new \InvalidArgumentException('cpr_children_codes must be a non-empty string or array of strings');
  197. }
  198. private function normalizeSemicolonList($value, string $field): string
  199. {
  200. if (is_array($value)) {
  201. $items = array_map(static fn($v) => trim((string)$v), array_values($value));
  202. $items = array_values(array_filter($items, static fn($v) => $v !== ''));
  203. if (!$items) {
  204. throw new \InvalidArgumentException("{$field} must not be empty");
  205. }
  206. return implode('; ', $items);
  207. }
  208. if (is_string($value)) {
  209. $trimmed = trim($value);
  210. if ($trimmed === '') {
  211. throw new \InvalidArgumentException("{$field} must not be empty");
  212. }
  213. $parts = preg_split('/\s*;\s*/', $trimmed) ?: [];
  214. $parts = array_map(static fn($v) => trim((string)$v), $parts);
  215. $parts = array_values(array_filter($parts, static fn($v) => $v !== ''));
  216. return implode('; ', $parts);
  217. }
  218. throw new \InvalidArgumentException("{$field} must be a non-empty string or array of strings");
  219. }
  220. private function getSemicolonListColumns(): array
  221. {
  222. return [
  223. 'cpr_collateral_type_code',
  224. 'cpr_collateral_type_name',
  225. 'cpr_constitution_process_indicator',
  226. 'cpr_otc_bondsman_account_code',
  227. 'cpr_issuer_name',
  228. 'cpr_issuers_document_number',
  229. 'cpr_issuers_person_type_acronym',
  230. 'cpr_issuer_legal_nature_code',
  231. 'cpr_issuers_state_acronym',
  232. 'cpr_issuers_city_name',
  233. 'cpr_production_place_name',
  234. 'cpr_property_registration_number',
  235. 'cpr_notary_name',
  236. 'cpr_total_production_area_in_hectares_number',
  237. 'cpr_total_area_in_hectares_number',
  238. 'cpr_car_code',
  239. 'cpr_latitude_code',
  240. 'cpr_longitude_code',
  241. 'cpr_zip_code',
  242. ];
  243. }
  244. private function flattenB3Arrays(array $data): array
  245. {
  246. if (!array_key_exists('collaterals', $data) && !array_key_exists('issuers', $data) && !array_key_exists('productionPlaces', $data)) {
  247. return $data;
  248. }
  249. if (array_key_exists('collaterals', $data) && !array_key_exists('cpr_collateral_type_code', $data)) {
  250. $collaterals = $data['collaterals'];
  251. if (is_array($collaterals) && $collaterals && $this->isAssoc($collaterals)) {
  252. $collaterals = [$collaterals];
  253. }
  254. if (is_array($collaterals)) {
  255. $data['cpr_collateral_type_code'] = array_map(static fn($c) => $c['collateralTypeCode'] ?? null, $collaterals);
  256. $data['cpr_collateral_type_name'] = array_map(static fn($c) => $c['collateralTypeName'] ?? null, $collaterals);
  257. $data['cpr_constitution_process_indicator'] = array_map(static fn($c) => $c['constitutionProcessIndicator'] ?? null, $collaterals);
  258. $data['cpr_otc_bondsman_account_code'] = array_map(static fn($c) => $c['otcBondsmanAccountCode'] ?? null, $collaterals);
  259. }
  260. }
  261. if (array_key_exists('issuers', $data) && !array_key_exists('cpr_issuer_name', $data)) {
  262. $issuers = $data['issuers'];
  263. if (is_array($issuers) && $issuers && $this->isAssoc($issuers)) {
  264. $issuers = [$issuers];
  265. }
  266. if (is_array($issuers)) {
  267. $data['cpr_issuer_name'] = array_map(static fn($i) => $i['cprIssuerName'] ?? null, $issuers);
  268. $data['cpr_issuers_document_number'] = array_map(static fn($i) => $i['documentNumber'] ?? null, $issuers);
  269. $data['cpr_issuers_person_type_acronym'] = array_map(static fn($i) => $i['personTypeAcronym'] ?? null, $issuers);
  270. $data['cpr_issuer_legal_nature_code'] = array_map(static fn($i) => $i['issuerLegalNatureCode'] ?? null, $issuers);
  271. $data['cpr_issuers_state_acronym'] = array_map(static fn($i) => $i['stateAcronym'] ?? null, $issuers);
  272. $data['cpr_issuers_city_name'] = array_map(static fn($i) => $i['cityName'] ?? null, $issuers);
  273. }
  274. }
  275. if (array_key_exists('productionPlaces', $data) && !array_key_exists('cpr_production_place_name', $data)) {
  276. $productionPlaces = $data['productionPlaces'];
  277. if (is_array($productionPlaces) && $productionPlaces && $this->isAssoc($productionPlaces)) {
  278. $productionPlaces = [$productionPlaces];
  279. }
  280. if (is_array($productionPlaces)) {
  281. $data['cpr_production_place_name'] = array_map(static fn($p) => $p['productionPlaceName'] ?? null, $productionPlaces);
  282. $data['cpr_property_registration_number'] = array_map(static fn($p) => $p['propertyRegistrationNumber'] ?? null, $productionPlaces);
  283. $data['cpr_notary_name'] = array_map(static fn($p) => $p['notaryName'] ?? null, $productionPlaces);
  284. $data['cpr_total_production_area_in_hectares_number'] = array_map(static fn($p) => $p['totalProductionAreaInHectaresNumber'] ?? null, $productionPlaces);
  285. $data['cpr_total_area_in_hectares_number'] = array_map(static fn($p) => $p['totalAreaInHectaresNumber'] ?? null, $productionPlaces);
  286. $data['cpr_car_code'] = array_map(static fn($p) => $p['carCode'] ?? null, $productionPlaces);
  287. $data['cpr_latitude_code'] = array_map(static fn($p) => $p['latitudeCode'] ?? null, $productionPlaces);
  288. $data['cpr_longitude_code'] = array_map(static fn($p) => $p['longitudeCode'] ?? null, $productionPlaces);
  289. $data['cpr_zip_code'] = array_map(static fn($p) => $p['zipCode'] ?? null, $productionPlaces);
  290. }
  291. }
  292. return $data;
  293. }
  294. private function isAssoc(array $arr): bool
  295. {
  296. return array_keys($arr) !== range(0, count($arr) - 1);
  297. }
  298. /**
  299. * @return array<int, string>|string
  300. */
  301. private function decodeChildrenCodes(string $stored)
  302. {
  303. $decoded = json_decode($stored, true);
  304. if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
  305. return $decoded;
  306. }
  307. return $stored;
  308. }
  309. private function fetchById(int $id): ?array
  310. {
  311. $stmt = $this->pdo->prepare('SELECT * FROM "cpr" WHERE cpr_id = :id');
  312. $stmt->execute(['id' => $id]);
  313. $record = $stmt->fetch(\PDO::FETCH_ASSOC);
  314. return $record ?: null;
  315. }
  316. public function findByPaymentId(int $paymentId): ?array
  317. {
  318. $stmt = $this->pdo->prepare('SELECT * FROM "cpr" WHERE payment_id = :payment_id ORDER BY cpr_id DESC LIMIT 1');
  319. $stmt->execute(['payment_id' => $paymentId]);
  320. $record = $stmt->fetch(\PDO::FETCH_ASSOC);
  321. if (!$record) {
  322. return null;
  323. }
  324. if (isset($record['cpr_children_codes'])) {
  325. $record['cpr_children_codes'] = $this->decodeChildrenCodes((string)$record['cpr_children_codes']);
  326. }
  327. return $record;
  328. }
  329. }