Spaces:
Running
Running
| """ | |
| InvoiceForge AI β validators/gst_validator.py | |
| GSTIN format and checksum validation with state code lookup. | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from dataclasses import dataclass | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CONSTANTS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GSTIN_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" | |
| STATE_CODES: dict[str, str] = { | |
| "01": "Jammu & Kashmir", | |
| "02": "Himachal Pradesh", | |
| "03": "Punjab", | |
| "04": "Chandigarh", | |
| "05": "Uttarakhand", | |
| "06": "Haryana", | |
| "07": "Delhi", | |
| "08": "Rajasthan", | |
| "09": "Uttar Pradesh", | |
| "10": "Bihar", | |
| "11": "Sikkim", | |
| "12": "Arunachal Pradesh", | |
| "13": "Nagaland", | |
| "14": "Manipur", | |
| "15": "Mizoram", | |
| "16": "Tripura", | |
| "17": "Meghalaya", | |
| "18": "Assam", | |
| "19": "West Bengal", | |
| "20": "Jharkhand", | |
| "21": "Odisha", | |
| "22": "Chhattisgarh", | |
| "23": "Madhya Pradesh", | |
| "24": "Gujarat", | |
| "25": "Daman & Diu", | |
| "26": "Dadra & Nagar Haveli", | |
| "27": "Maharashtra", | |
| "28": "Andhra Pradesh (Old)", | |
| "29": "Karnataka", | |
| "30": "Goa", | |
| "31": "Lakshadweep", | |
| "32": "Kerala", | |
| "33": "Tamil Nadu", | |
| "34": "Puducherry", | |
| "35": "Andaman & Nicobar Islands", | |
| "36": "Telangana", | |
| "37": "Andhra Pradesh", | |
| } | |
| GSTIN_PATTERN = re.compile( | |
| r"^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d]Z[A-Z\d]$" | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # DATA CLASS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class GSTINValidationResult: | |
| """Result of GSTIN validation.""" | |
| gstin: str | |
| is_valid: bool | |
| state_code: str | |
| state_name: str | |
| pan: str | |
| entity_type: str # P=Proprietor, C=Company, etc. | |
| checksum_valid: bool | |
| errors: list[str] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # VALIDATOR | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class GSTValidator: | |
| """ | |
| Validates GSTIN numbers for format correctness and checksum integrity. | |
| """ | |
| def validate(self, gstin: str) -> GSTINValidationResult: | |
| """ | |
| Full GSTIN validation. | |
| Args: | |
| gstin: 15-character GSTIN string (uppercase, no spaces). | |
| Returns: | |
| GSTINValidationResult with all validation details. | |
| """ | |
| errors: list[str] = [] | |
| gstin = gstin.strip().upper() | |
| # ββ Length check βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if len(gstin) != 15: | |
| errors.append(f"GSTIN must be 15 characters; got {len(gstin)}.") | |
| return self._fail_result(gstin, errors) | |
| # ββ Pattern check ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if not GSTIN_PATTERN.match(gstin): | |
| errors.append("GSTIN format invalid. Expected: 2D 5A 4D 1A 1Z_ Z 1_") | |
| return self._fail_result(gstin, errors) | |
| # ββ State code check βββββββββββββββββββββββββββββββββββββββββββββββ | |
| state_code = gstin[:2] | |
| state_name = STATE_CODES.get(state_code, "") | |
| if not state_name: | |
| errors.append(f"Unknown state code: {state_code}.") | |
| # ββ PAN embedded βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| pan = gstin[2:12] | |
| pan_valid = bool(re.match(r"^[A-Z]{5}\d{4}[A-Z]$", pan)) | |
| if not pan_valid: | |
| errors.append(f"Embedded PAN invalid: {pan}.") | |
| # ββ Entity type ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| entity_type_char = gstin[11] | |
| entity_type = _entity_type_label(entity_type_char) | |
| # ββ Z check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if gstin[13] != "Z": | |
| errors.append("Position 14 (index 13) must be 'Z'.") | |
| # ββ Checksum βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| checksum_valid = self._verify_checksum(gstin) | |
| if not checksum_valid: | |
| errors.append("GSTIN checksum digit is incorrect.") | |
| is_valid = len(errors) == 0 | |
| return GSTINValidationResult( | |
| gstin=gstin, | |
| is_valid=is_valid, | |
| state_code=state_code, | |
| state_name=state_name, | |
| pan=pan, | |
| entity_type=entity_type, | |
| checksum_valid=checksum_valid, | |
| errors=errors, | |
| ) | |
| def validate_batch(self, gstins: list[str]) -> list[GSTINValidationResult]: | |
| """Validate a list of GSTINs and return results in the same order.""" | |
| return [self.validate(g) for g in gstins] | |
| def _verify_checksum(gstin: str) -> bool: | |
| """Modulo-36 weighted checksum (GST circular reference algorithm).""" | |
| factor = 2 | |
| total = 0 | |
| for char in gstin[:-1]: | |
| idx = GSTIN_CHARS.find(char) | |
| if idx < 0: | |
| return False | |
| val = idx * factor | |
| total += (val // 36) + (val % 36) | |
| factor = 3 if factor == 2 else 2 | |
| check_val = (36 - (total % 36)) % 36 | |
| expected = GSTIN_CHARS[check_val] | |
| return expected == gstin[-1] | |
| def _fail_result(gstin: str, errors: list[str]) -> GSTINValidationResult: | |
| return GSTINValidationResult( | |
| gstin=gstin, | |
| is_valid=False, | |
| state_code="", | |
| state_name="", | |
| pan="", | |
| entity_type="", | |
| checksum_valid=False, | |
| errors=errors, | |
| ) | |
| def _entity_type_label(char: str) -> str: | |
| """Map entity type character to a human-readable label.""" | |
| mapping = { | |
| "P": "Proprietorship", | |
| "C": "Company", | |
| "H": "HUF", | |
| "F": "Firm", | |
| "A": "AOP/BOI", | |
| "T": "Trust/AOP", | |
| "B": "Local Authority", | |
| "L": "Local Authority", | |
| "J": "Artificial Juridical Person", | |
| "G": "Government", | |
| } | |
| return mapping.get(char, f"Entity Type {char}") | |