A Daily - Yash-777/MyWorld GitHub Wiki

import javax.net.ssl.; import java.io.; import java.net.; import java.nio.charset.StandardCharsets; import java.nio.file.; import java.security.; import java.security.cert.; import java.security.cert.Certificate; import java.util.*; import java.util.Base64;

/**

  • SslFetchTest β€” Standalone Java 8 program
  • Reproduces and fixes the PKIX error when fetching from:
  • Run WITHOUT any special JVM flags or keytool steps.
  • Compile: javac SslFetchTest.java
  • Run: java SslFetchTest
  • What it does:
    1. Loads JVM cacerts as base truststore (preserves all built-in CAs)
    1. For each target URL, opens a trust-all socket to capture the live cert chain
    1. Imports every cert into the in-memory truststore (NEW / SKIP / UPDATE)
    1. Installs the patched truststore as the JVM-wide SSLContext default
    1. Fetches each URL using HttpsURLConnection and prints status + first 500 chars
    1. Exports .cer files to exportedcerts/ for inspection */ public class SslFetchTest {
// ── Target URLs ──────────────────────────────────────────────────────────
private static final String[] TARGET_URLS = {
    "https://azuredownloads-g3ahgwb5b8bkbxhd.b01.azurefd.net/github-copilot/content.xml",
    "https://marketplace.eclipse.org/content/github-copilot",
    "https://docs.github.com/en/copilot/how-tos/get-code-suggestions/get-ide-code-suggestions"
};

private static final String EXPORT_DIR      = "exportedcerts";
private static final int    CONNECT_TIMEOUT  = 15_000; // ms
private static final int    READ_TIMEOUT     = 15_000; // ms

// ─────────────────────────────────────────────────────────────────────────

public static void main(String[] args) throws Exception {
    printBanner();

    // Step 1 β€” create export directory
    new File(EXPORT_DIR).mkdirs();

    // Step 2 β€” load JVM cacerts as the starting truststore
    KeyStore trustStore = loadJvmCacerts();

    // Step 3 β€” for every target URL, fetch and import its cert chain
    boolean anyModified = false;
    for (String url : TARGET_URLS) {
        String host = extractHost(url);
        int    port = 443;
        System.out.println("\n── Processing: " + host + ":" + port);
        List<X509Certificate> chain = fetchCertChain(host, port);
        if (chain.isEmpty()) {
            System.out.println("   [WARN]  No certs fetched β€” host unreachable or refused");
        } else {
            boolean modified = syncToTrustStore(trustStore, host, chain);
            if (modified) anyModified = true;
        }
    }

    // Step 4 β€” install patched truststore as JVM-wide default
    if (anyModified) {
        installAsJvmDefault(trustStore);
        persistJks(trustStore);
    } else {
        System.out.println("\n[INFO] All certs already up-to-date β€” re-installing anyway to be safe");
        installAsJvmDefault(trustStore);
    }

    // Step 5 β€” now actually fetch each URL with the patched truststore
    System.out.println("\n" + repeat("═", 60));
    System.out.println("  FETCHING URLS");
    System.out.println(repeat("═", 60));

    for (String url : TARGET_URLS) {
        fetchAndPrint(url);
    }

    System.out.println("\n[INFO] Done. Exported certs: " + new File(EXPORT_DIR).getAbsolutePath());
    System.out.println("[INFO] Inspect JKS: keytool -list -keystore "
            + EXPORT_DIR + "/combined-truststore.jks -storepass changeit");
}

// =========================================================================
//  Step A β€” Load JVM cacerts
// =========================================================================

/**
 * Loads $JAVA_HOME/lib/security/cacerts (Java 11+) or
 * $JAVA_HOME/jre/lib/security/cacerts (Java 8) as the base KeyStore.
 * Password is always "changeit".
 */
private static KeyStore loadJvmCacerts() throws Exception {
    String javaHome = System.getProperty("java.home");
    File cacerts = new File(javaHome, "lib/security/cacerts");
    if (!cacerts.exists()) {
        cacerts = new File(javaHome, "jre/lib/security/cacerts");
    }
    if (!cacerts.exists()) {
        throw new FileNotFoundException("Cannot find cacerts at: " + javaHome);
    }
    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
    try (FileInputStream fis = new FileInputStream(cacerts)) {
        ks.load(fis, "changeit".toCharArray());
    }
    System.out.println("[INFO] Loaded JVM cacerts: " + cacerts.getAbsolutePath()
            + " (" + ks.size() + " entries)");
    return ks;
}

// =========================================================================
//  Step B β€” Capture cert chain via trust-all socket
// =========================================================================

/**
 * Opens a temporary TLS socket using a no-validation TrustManager so we can
 * capture the certificate chain regardless of whether it's currently trusted.
 * The socket is closed immediately after the handshake β€” only the chain is kept.
 *
 * SECURITY NOTE: This trust-all approach is used ONLY here during bootstrap
 * to read the chain. Once imported, all real connections use proper validation.
 */
private static List<X509Certificate> fetchCertChain(String host, int port) {
    List<X509Certificate> chain = new ArrayList<X509Certificate>();
    try {
        // Capture array β€” lambdas not available in Java 8, use array trick
        final X509Certificate[][] captured = new X509Certificate[1][];

        SSLContext trustAll = SSLContext.getInstance("TLS");
        trustAll.init(null, new TrustManager[]{new X509TrustManager() {
            public void checkClientTrusted(X509Certificate[] c, String a) {}
            public void checkServerTrusted(X509Certificate[] c, String a) { captured[0] = c; }
            public X509Certificate[] getAcceptedIssuers() {
                return captured[0] != null ? captured[0] : new X509Certificate[0];
            }
        }}, new SecureRandom());

        SSLSocket socket = (SSLSocket) trustAll.getSocketFactory().createSocket(host, port);
        try {
            socket.setSoTimeout(CONNECT_TIMEOUT);
            socket.startHandshake();
            for (Certificate c : socket.getSession().getPeerCertificates()) {
                if (c instanceof X509Certificate) chain.add((X509Certificate) c);
            }
        } finally {
            try { socket.close(); } catch (Exception ignored) {}
        }

        System.out.println("   [INFO]  Fetched " + chain.size() + " cert(s) from " + host + ":" + port);
        for (int i = 0; i < chain.size(); i++) {
            X509Certificate c = chain.get(i);
            System.out.println("           cert[" + i + "] Subject : " + c.getSubjectDN().getName());
            System.out.println("                    Issuer  : " + c.getIssuerDN().getName());
            System.out.println("                    Expires : " + c.getNotAfter());
        }

    } catch (Exception e) {
        System.out.println("   [ERROR] Cert fetch failed: " + e.getMessage());
    }
    return chain;
}

// =========================================================================
//  Step C β€” NEW / SKIP / UPDATE sync
// =========================================================================

private static boolean syncToTrustStore(KeyStore ks, String host,
                                         List<X509Certificate> chain) {
    boolean modified = false;
    for (int i = 0; i < chain.size(); i++) {
        X509Certificate cert     = chain.get(i);
        String          fileName = host + "-cert-" + i + ".cer";
        File            file     = new File(EXPORT_DIR, fileName);
        String          alias    = host.replace(".", "-") + "-" + i;
        try {
            byte[] liveBytes = cert.getEncoded();

            if (!file.exists()) {
                System.out.println("   [NEW]    " + fileName);
                writeCertFile(file, liveBytes);
                addAlias(ks, alias, cert);
                modified = true;

            } else if (file.length() != liveBytes.length) {
                System.out.println("   [UPDATE] " + fileName
                        + " (disk=" + file.length() + "B live=" + liveBytes.length + "B)");
                writeCertFile(file, liveBytes);
                removeAlias(ks, alias);
                addAlias(ks, alias, cert);
                modified = true;

            } else {
                System.out.println("   [SKIP]   " + fileName + " (" + file.length() + "B β€” unchanged)");
            }

        } catch (Exception e) {
            System.out.println("   [ERROR]  cert[" + i + "] " + host + ": " + e.getMessage());
        }
    }
    return modified;
}

// =========================================================================
//  Step D β€” Install patched truststore as JVM default
// =========================================================================

private static void installAsJvmDefault(KeyStore ts) throws Exception {
    TrustManagerFactory tmf =
            TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(ts);
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(null, tmf.getTrustManagers(), new SecureRandom());
    SSLContext.setDefault(ctx);
    HttpsURLConnection.setDefaultSSLSocketFactory(ctx.getSocketFactory());
    // Also set HostnameVerifier to standard (not trust-all)
    HttpsURLConnection.setDefaultHostnameVerifier(new DefaultHostnameVerifier());
    System.out.println("\n[INFO] JVM SSLContext patched (" + ts.size() + " trust entries)");
}

// =========================================================================
//  Step E β€” Fetch URL and print result
// =========================================================================

private static void fetchAndPrint(String urlStr) {
    System.out.println("\n  URL : " + urlStr);
    System.out.println("  " + repeat("─", 56));
    try {
        URL url = new URL(urlStr);
        HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
        conn.setConnectTimeout(CONNECT_TIMEOUT);
        conn.setReadTimeout(READ_TIMEOUT);
        conn.setRequestMethod("GET");
        conn.setRequestProperty("User-Agent",
                "Mozilla/5.0 (SslFetchTest/1.0; Java/" + System.getProperty("java.version") + ")");
        conn.setRequestProperty("Accept", "*/*");

        int status = conn.getResponseCode();
        System.out.println("  HTTP Status : " + status + " " + conn.getResponseMessage());
        System.out.println("  Content-Type: " + conn.getContentType());

        // Read body (use error stream on non-2xx)
        InputStream is = (status >= 200 && status < 300)
                ? conn.getInputStream() : conn.getErrorStream();

        if (is != null) {
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(is, StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            String line;
            int chars = 0;
            while ((line = reader.readLine()) != null && chars < 800) {
                sb.append(line).append("\n");
                chars += line.length();
            }
            reader.close();
            System.out.println("  Body (first 800 chars):");
            System.out.println("  β”Œ" + repeat("─", 55));
            for (String l : sb.toString().split("\n")) {
                System.out.println("  β”‚ " + l);
            }
            System.out.println("  β””" + repeat("─", 55));
        }

        conn.disconnect();

    } catch (SSLHandshakeException e) {
        System.out.println("  [FAIL] SSLHandshakeException β€” PKIX still failing: " + e.getMessage());
        System.out.println("         This means the ZScaler/proxy cert was not captured.");
        System.out.println("         Check exportedcerts/ and run: keytool -list -keystore "
                + EXPORT_DIR + "/combined-truststore.jks -storepass changeit");
    } catch (Exception e) {
        System.out.println("  [FAIL] " + e.getClass().getSimpleName() + ": " + e.getMessage());
    }
}

// =========================================================================
//  Helpers
// =========================================================================

private static String extractHost(String url) {
    // Strip scheme and path: https://host.example.com/path β†’ host.example.com
    String s = url;
    if (s.startsWith("https://")) s = s.substring(8);
    if (s.startsWith("http://"))  s = s.substring(7);
    int slash = s.indexOf('/');
    if (slash != -1) s = s.substring(0, slash);
    int colon = s.indexOf(':');
    if (colon != -1) s = s.substring(0, colon);
    return s.trim();
}

private static void writeCertFile(File file, byte[] encoded) throws IOException {
    String pem = "-----BEGIN CERTIFICATE-----\n"
            + Base64.getMimeEncoder(64, new byte[]{'\n'}).encodeToString(encoded)
            + "\n-----END CERTIFICATE-----\n";
    try (FileWriter fw = new FileWriter(file)) { fw.write(pem); }
}

private static void addAlias(KeyStore ks, String alias, X509Certificate cert)
        throws KeyStoreException {
    if (!ks.containsAlias(alias)) {
        ks.setCertificateEntry(alias, cert);
        System.out.println("           keystore ← '" + alias + "'");
    }
}

private static void removeAlias(KeyStore ks, String alias) throws KeyStoreException {
    if (ks.containsAlias(alias)) ks.deleteEntry(alias);
}

private static void persistJks(KeyStore ks) {
    File jks = new File(EXPORT_DIR, "combined-truststore.jks");
    try (FileOutputStream fos = new FileOutputStream(jks)) {
        ks.store(fos, "changeit".toCharArray());
        System.out.println("[INFO] JKS saved β†’ " + jks.getAbsolutePath());
    } catch (Exception e) {
        System.out.println("[WARN] JKS save failed: " + e.getMessage());
    }
}

private static String repeat(String s, int n) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < n; i++) sb.append(s);
    return sb.toString();
}

private static void printBanner() {
    System.out.println(repeat("═", 60));
    System.out.println("  SslFetchTest β€” PKIX Auto-Trust + URL Fetch");
    System.out.println("  Java : " + System.getProperty("java.version")
            + "  OS: " + System.getProperty("os.name"));
    System.out.println("  JAVA_HOME : " + System.getProperty("java.home"));
    System.out.println(repeat("═", 60));
    System.out.println("  Target URLs:");
    for (String u : TARGET_URLS) System.out.println("    β€’ " + u);
    System.out.println(repeat("═", 60));
}

// ── Standard hostname verifier (replaces the JVM default after patching) ──

/**
 * RFC 2818-compliant hostname verifier.
 * Replaces the JVM default after we call setDefaultSSLSocketFactory()
 * to avoid accidentally leaving a trust-all verifier in place.
 */
static class DefaultHostnameVerifier implements HostnameVerifier {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        try {
            // Delegate to the standard HTTPS hostname verifier
            HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
            return true;
        } catch (Exception e) {
            // Fall back to javax built-in
            try {
                javax.net.ssl.HostnameVerifier hv =
                        HttpsURLConnection.getDefaultHostnameVerifier();
                return hv.verify(hostname, session);
            } catch (Exception ex) {
                return false;
            }
        }
    }
}

}

package com.royalenfield.finance.util;

import java.util.Set;

/**

  • PanValidatorUtil
  • Offline utility for validating Indian PAN (Permanent Account Number) structure
  • before making any downstream API calls (e.g., IDFC CREATELOAN).
  • PAN Format: AAAAA9999A
  • [0-2] β†’ 3 alphabetic chars : Sequential series (AAA–ZZZ)
  • [3] β†’ 1 alphabetic char : Entity/Holder type (P, F, C, H, A, T, B, G, J, L)
  • [4] β†’ 1 alphabetic char : First letter of surname (Person) or entity name
  • [5-8] β†’ 4 numeric digits : Sequential number (0001–9999)
  • [9] β†’ 1 alphabetic char : Alphabetic check digit
  • Reference:
  • https://www.razehq.com/blog/Key-Patterns-in-PAN-Aadhaar-CIN-and-GSTIN-for-robust-Due-Diligence */ public final class PanValidatorUtil {
// -------------------------------------------------------------------------
// Regex
// -------------------------------------------------------------------------

/** Base structural regex β€” 5 letters, 4 digits, 1 letter. */
private static final String PAN_CARD_REGEX = "^[A-Z]{5}[0-9]{4}[A-Z]{1}$";

// -------------------------------------------------------------------------
// Enums
// -------------------------------------------------------------------------

/**
 * Error codes aligned with the existing finance error catalogue.
 */
public enum PanError {

    /** PAN is null or blank. */
    INVALID_PAN_BLANK("1025", "PAN Card Number Is Required."),

    /** PAN does not match the base 10-char alphanumeric pattern. */
    INVALID_PAN_CARD_PATTERN("1029", "PAN Card Number Is Invalid."),

    /**
     * 4th character (index 3) is not a recognised entity/holder type.
     * Valid: A, B, C, F, G, H, J, L, P, T
     */
    INVALID_PAN_ENTITY_TYPE("1030", "PAN Entity/Holder Type Character (4th) Is Invalid."),

    /**
     * 5th character (index 4) does not match the first letter of the
     * provided surname (for Persons) or entity name.
     */
    INVALID_PAN_NAME_INITIAL("1031", "PAN 5th Character Does Not Match Applicant Surname Initial.");

    private final String code;
    private final String message;

    PanError(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String getCode()    { return code; }
    public String getMessage() { return message; }

    @Override
    public String toString() {
        return "[" + code + "] " + message;
    }
}

/**
 * Holder/Entity type encoded at PAN[3] (4th character).
 *
 * Per Income Tax Dept rules:
 *   P – Person (Individual)
 *   F – Firm
 *   C – Company / Corporation
 *   H – Hindu Undivided Family (HUF)
 *   A – Association of Persons (AOP)
 *   T – Trust / AOP (Trust)
 *   B – Body of Individuals (BOI)
 *   G – Government entity
 *   J – Artificial Juridical Person
 *   L – Local Authority
 */
public enum PanEntityType {
    P("Person / Individual"),
    F("Firm"),
    C("Company / Corporation"),
    H("Hindu Undivided Family (HUF)"),
    A("Association of Persons (AOP)"),
    T("Trust"),
    B("Body of Individuals (BOI)"),
    G("Government Entity"),
    J("Artificial Juridical Person"),
    L("Local Authority");

    private final String description;

    PanEntityType(String description) {
        this.description = description;
    }

    public String getDescription() { return description; }

    /** Returns true when the character maps to an Individual / Person holder. */
    public boolean isPerson() {
        return this == P;
    }

    /** Returns true when the PAN belongs to a non-individual business entity. */
    public boolean isNonPerson() {
        return !isPerson();
    }
}

// -------------------------------------------------------------------------
// Result
// -------------------------------------------------------------------------

/**
 * Immutable result object returned by every public validate* method.
 */
public static final class PanValidationResult {

    private final boolean valid;
    private final PanError error;          // null when valid == true
    private final PanEntityType entityType; // null when PAN is structurally invalid

    private PanValidationResult(boolean valid, PanError error, PanEntityType entityType) {
        this.valid      = valid;
        this.error      = error;
        this.entityType = entityType;
    }

    static PanValidationResult ok(PanEntityType entityType) {
        return new PanValidationResult(true, null, entityType);
    }

    static PanValidationResult fail(PanError error) {
        return new PanValidationResult(false, error, null);
    }

    static PanValidationResult fail(PanError error, PanEntityType entityType) {
        return new PanValidationResult(false, error, entityType);
    }

    public boolean isValid()               { return valid; }
    public PanError getError()             { return error; }
    public PanEntityType getEntityType()   { return entityType; }

    /** Convenience: error code string, or null if valid. */
    public String getErrorCode()    { return error != null ? error.getCode()    : null; }
    public String getErrorMessage() { return error != null ? error.getMessage() : null; }

    @Override
    public String toString() {
        return valid
                ? "PanValidationResult{valid=true, entityType=" + entityType + "}"
                : "PanValidationResult{valid=false, error=" + error + "}";
    }
}

// -------------------------------------------------------------------------
// Valid entity-type characters (for fast lookup)
// -------------------------------------------------------------------------

private static final Set<Character> VALID_ENTITY_CHARS;

static {
    VALID_ENTITY_CHARS = new java.util.HashSet<>();
    for (PanEntityType t : PanEntityType.values()) {
        VALID_ENTITY_CHARS.add(t.name().charAt(0));
    }
}

// -------------------------------------------------------------------------
// Constructor β€” not instantiable
// -------------------------------------------------------------------------

private PanValidatorUtil() {
    throw new UnsupportedOperationException("Utility class");
}

// =========================================================================
// PUBLIC API
// =========================================================================

/**
 * Validates PAN structure only (pattern + 4th char + 5th char NOT checked
 * against any name). Use when no applicant name is available.
 *
 * Checks:
 *  1. Non-blank
 *  2. Matches ^[A-Z]{5}[0-9]{4}[A-Z]{1}$  (INVALID_PAN_CARD_PATTERN)
 *  3. 4th character is a valid entity type  (INVALID_PAN_ENTITY_TYPE)
 *
 * @param pan Raw PAN string from request payload (may be null).
 * @return {@link PanValidationResult}
 */
public static PanValidationResult validatePattern(String pan) {
    return doValidate(pan, null, null);
}

/**
 * Validates PAN and cross-checks the 5th character against the applicant's
 * last name initial. Intended for Individual (P-type) PANs.
 *
 * Checks (in order):
 *  1. Non-blank
 *  2. Matches base pattern                  (INVALID_PAN_CARD_PATTERN)
 *  3. 4th character is a valid entity type  (INVALID_PAN_ENTITY_TYPE)
 *  4. 5th character == first letter of lastName (INVALID_PAN_NAME_INITIAL)
 *
 * @param pan       PAN from payload.
 * @param firstName Applicant first name (used for context; not validated in PAN).
 * @param lastName  Applicant last/surname β€” its initial must match PAN[4].
 * @return {@link PanValidationResult}
 */
public static PanValidationResult validateWithName(String pan,
                                                   String firstName,
                                                   String lastName) {
    return doValidate(pan, firstName, lastName);
}

/**
 * Returns the {@link PanEntityType} decoded from a structurally valid PAN,
 * or {@code null} if the PAN is invalid.
 *
 * @param pan PAN string (will be normalised to uppercase).
 */
public static PanEntityType getEntityType(String pan) {
    if (pan == null || pan.isBlank()) return null;
    String normalised = pan.trim().toUpperCase();
    if (!normalised.matches(PAN_CARD_REGEX)) return null;
    char entityChar = normalised.charAt(3);
    try {
        return PanEntityType.valueOf(String.valueOf(entityChar));
    } catch (IllegalArgumentException e) {
        return null;
    }
}

/**
 * Quick boolean check β€” returns {@code true} only when the PAN passes all
 * structural validations (pattern + 4th char). Does NOT check name initial.
 *
 * @param pan PAN string.
 */
public static boolean isValidPan(String pan) {
    return validatePattern(pan).isValid();
}

// =========================================================================
// PRIVATE HELPERS
// =========================================================================

private static PanValidationResult doValidate(String pan,
                                               String firstName,
                                               String lastName) {
    // ── Step 1: Null / blank guard
    if (pan == null || pan.isBlank()) {
        return PanValidationResult.fail(PanError.INVALID_PAN_BLANK);
    }

    final String normalised = pan.trim().toUpperCase();

    // ── Step 2: Base pattern  ^[A-Z]{5}[0-9]{4}[A-Z]{1}$
    if (!normalised.matches(PAN_CARD_REGEX)) {
        return PanValidationResult.fail(PanError.INVALID_PAN_CARD_PATTERN);
    }

    // ── Step 3: 4th character β€” entity / holder type
    char entityChar = normalised.charAt(3);
    PanEntityType entityType;
    try {
        entityType = PanEntityType.valueOf(String.valueOf(entityChar));
    } catch (IllegalArgumentException e) {
        // Character not in enum β†’ invalid
        return PanValidationResult.fail(PanError.INVALID_PAN_ENTITY_TYPE);
    }

    // ── Step 4: 5th character β€” surname / entity-name initial (only when name supplied)
    if (lastName != null && !lastName.isBlank()) {
        char panNameInitial    = normalised.charAt(4);
        char applicantInitial  = Character.toUpperCase(lastName.trim().charAt(0));

        if (panNameInitial != applicantInitial) {
            // Return entity type alongside the error so caller still knows the type
            return PanValidationResult.fail(PanError.INVALID_PAN_NAME_INITIAL, entityType);
        }
    }

    return PanValidationResult.ok(entityType);
}

// =========================================================================
// QUICK DEMO (remove before production)
// =========================================================================

public static void main(String[] args) {
    System.out.println("=== PanValidatorUtil Demo ===\n");

    record TestCase(String label, String pan, String firstName, String lastName) {}

    java.util.List<TestCase> cases = java.util.List.of(
        new TestCase("Valid individual (Jain β†’ J matches PAN[4])",
                     "BREJJ1274F", "Rajat", "Jain"),
        new TestCase("Invalid β€” PAN[3] 'Q' is not a valid entity char",
                     "BREQW1234F", "Rajat", "Jain"),
        new TestCase("Invalid β€” PAN[3] 'M' is not a valid entity char",
                     "BREMW1274F", "Rajat", "Jain"),
        new TestCase("Invalid β€” name initial mismatch (surname=Jain, PAN[4]=W)",
                     "BREPW1234F", "Rajat", "Jain"),
        new TestCase("Valid company PAN (no name check)",
                     "AABCS1234A", null, null),
        new TestCase("Invalid β€” pattern too short",
                     "ABCD1234F", null, null),
        new TestCase("Blank PAN",
                     "", "Rajat", "Jain")
    );

    for (TestCase tc : cases) {
        PanValidationResult result = tc.lastName() != null
                ? validateWithName(tc.pan(), tc.firstName(), tc.lastName())
                : validatePattern(tc.pan());

        System.out.printf("[%-55s]%n  PAN: %-15s β†’ %s%n%n",
                tc.label(), tc.pan(), result);
    }
}

}

⚠️ **GitHub.com Fallback** ⚠️