Luhn's Algorithm: Credit Card Validation
Tags: programming, laravel, algorithms
Published: 2024-06-30 07:58:28

## The challenge

At some point in my backend journey, I was faced with the challenge to validate credit cards numbers, submitted by users on an application that processes subscription-based payment for a service.

Let’s imagine we are creating an e-commerce website. The site is looking good and we are ready to implement payments. The first step of the payment process is entering payment information. One of the possibilities is to pay with a credit card. We are using an external payment system to process the payments, and the external system has a limited number of payments it can process per time.

To reduce the strain on the external payment system (caused by incorrect credit card details being entered), we are supposed to implement a second validation system which runs on the server and is not limited. This system is intended to perform simple sanity checks of the credit card information and respond with either a success or failure.

The task is to implement a simple page with an input form to take in credit card information and send it to a Backend API for validation.

The Backend API should respond with either success or failure, and we should react appropriately in the Frontend (e.g. display a green check mark or a stop sign).

Here is an example of a validation algorithm that can be used:

1. The expiry date of the credit card (year and month) must be AFTER present time

2. The CVV (security code) of the credit card must be exactly 3 digits long

- Unless it’s an American Express card, in which case the CVV must be exactly 4 digits long

- American Express are cards whose PAN (card numbers) starts with either “34” or “37”

3. The PAN (card number) is between 16 and 19 digits long

4. Last digit of the PAN (card number) is checked using Luhn’s algorithm.

## Solution

I implemented the Luhn's algorithm to validate the credit card information requests coming from the frontend. Here's my implementation in PHP, Laravel to be specific.

```
<?php

namespace App\Http\Controllers;

use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;

class CreditCardController extends Controller
{
    public function validateCreditCard(Request $request): JsonResponse
    {
        $validator = Validator::make($request->all(), [
            'creditCardNumber' => [
                'required',
                'numeric',
                'digits_between:16,19',
                function ($attribute, $value, $fail) {
                    $cardNumbers = array_map('intval', array_reverse(str_split($value)));
                    for ($i = 0; $i < count($cardNumbers); $i++) {
                        if (($i % 2) !== 0) {
                            $cardNumbers[$i] *= 2;
                            if($cardNumbers[$i] > 9)
                            {
                                $cardNumbers[$i] -= 9;
                            }
                        }
                    }
                    if (array_sum($cardNumbers) % 10 !== 0) {
                        $fail('The :attribute is incorrect or invalid.');
                    }
                },
            ],
            'expiryDate' => [
                'required',
                'regex:/^(0[1-9]|1[0-2])\/([0-9]{2})$/',
                function ($attribute, $value, $fail) {
                    $expiry = Carbon::createFromFormat('m/y', $value);
                    $currentDate = Carbon::now();

                    if (!$expiry || $expiry->lessThan($currentDate)) {
                        $fail('The :attribute is invalid or has already expired.');
                    }
                },
            ],
            'cvv' => [
                'required',
                'numeric',
                function ($attribute, $value, $fail) use ($request){
                    $cardType = $this->getCardType($request->input('creditCardNumber'));
                    if ($cardType === 'amex') {
                        if (strlen($value) !== 4) {
                            $fail('The :attribute must be a 4-digit number for American Express cards.');
                        }
                    } else {
                        if (strlen($value) !== 3) {
                            $fail('The :attribute must be a 3-digit number.');
                        }
                    }
                }
            ],
        ]);

        if ($validator->fails()) {
            return response()->json(['errors' => $validator->errors()], 422);
        }
        return response()->json([...$validator->validated(), 'validation_success' => 'true'], 200);
    }

    private function getCardType($cardNumber)
    {
        $cardTypeDigits = substr($cardNumber, 0, 2);

        if (str_contains($cardTypeDigits, '34') || str_contains($cardTypeDigits, '37')) {
            return 'amex';
        } elseif (str_starts_with($cardNumber, '4')) {
            return 'visa';
        } elseif (str_starts_with($cardNumber, '5')) {
            return 'mastercard';
        }

        return 'unknown';
    }
}
```

I was delighted with my solution.

## Conclusion

After solving the task, it made me to want to learn more about backend engineering, and how complex systems work. I hope this [HNG program](https://hng.tech/internship) will provide the opportunity I need to deepen my knowledge on advanced backend concepts and get me my desired [job](https://hng.tech/hire). Thanks.