Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Click Integration Spring Boot Starter

A ready-to-use Spring Boot library for integrating the CLICK payment system into your Java application.

You do not need to understand the raw Click API. Just add this library, set your credentials, and write your business logic. The library handles all the HTTP communication, signature verification, and request/response mapping for you.


What is Click?

Click is a popular payment system in Uzbekistan. When a user pays through Click, the following happens behind the scenes:

  1. User initiates a payment via Click app, website, USSD, or Telegram bot.
  2. Click sends a "Prepare" request to your server — asking "Does this order exist? Is the amount correct?"
  3. Your server validates and responds.
  4. Click sends a "Complete" request to your server — telling you "Payment succeeded" or "Payment was cancelled."
  5. Your server confirms the order or cancels it.

This library automatically creates the endpoints that Click calls (steps 2 and 4) and handles all the security checks. You only need to write what happens with your orders (e.g., save to database, send confirmation email, etc.).

Additionally, this library provides a service for Merchant API — meaning you can also call Click's API directly from your code to create invoices, manage card tokens, cancel payments, and more.


Features

  • Automatic webhook endpoints/api/click/prepare and /api/click/complete are created for you
  • Signature verification — every incoming request from Click is verified using MD5 hash (you don't need to write this yourself)
  • Merchant API client — call Click's API to create invoices, check statuses, cancel payments, and manage card tokens
  • All request/response models included — every DTO (Data Transfer Object) for Click is already created as a Java class
  • Configure once in application.yml — your merchant credentials go in one place
  • You control your business logic — implement one interface and the library does the rest

How to Install

Step 1: Build this library

Clone this repository and install it to your local Maven repository:

mvn clean install

Step 2: Add the dependency to your project

In your Spring Boot project's pom.xml, add:

<dependency>
    <groupId>integration.payment</groupId>
    <artifactId>click-integration-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

That's it! Spring Boot will auto-detect and configure the library.


How to Configure

When you register with Click as a merchant, they give you 4 values:

  • merchant_id — your unique merchant identifier
  • service_id — the ID of the service you are selling
  • merchant_user_id — your user ID in Click's system
  • secret_key — a secret string used to verify requests (keep this safe!)

Put these values in your project's application.yml:

click:
  merchant-id: 12345
  service-id: 67890
  merchant-user-id: 11111
  secret-key: YOUR_SECRET_KEY_FROM_CLICK

Or if you prefer application.properties:

click.merchant-id=12345
click.service-id=67890
click.merchant-user-id=11111
click.secret-key=YOUR_SECRET_KEY_FROM_CLICK

Optional settings

Property Default Value Description
click.endpoint https://api.click.uz/v2/merchant/ Click's API base URL (you almost never change this)
click.shop-api.base-path /api/click The URL path where prepare/complete endpoints are registered

How to Use

There are two parts to this library:

  1. SHOP-API (incoming webhooks) — Click calls YOUR server. You must implement one interface.
  2. Merchant API (outgoing calls) — YOUR server calls Click. Just inject a service and call methods.

Part 1: Handle incoming payments from Click (SHOP-API)

This is the required part. Click needs to call your server to prepare and complete payments.

Step 1: Create a class that implements ClickOrderService

This is the only thing you need to write. The library calls your methods when Click sends requests.

import integration.payment.clickintegration.model.ClickErrorCode;
import integration.payment.clickintegration.model.shop.ClickCompleteRequest;
import integration.payment.clickintegration.model.shop.ClickPrepareRequest;
import integration.payment.clickintegration.service.ClickOrderResult;
import integration.payment.clickintegration.service.ClickOrderService;
import org.springframework.stereotype.Service;

@Service
public class MyOrderService implements ClickOrderService {

    // You can inject your own repositories, services, etc.
    // private final OrderRepository orderRepository;

    /**
     * Called FIRST to check if the order exists and the amount is correct.
     * This is called before both Prepare and Complete.
     *
     * @param merchantTransId - this is YOUR order ID (the one you gave to Click)
     * @param amount          - the payment amount that Click is trying to charge
     * @return ClickOrderResult - return ok() if valid, or error() if not
     */
    @Override
    public ClickOrderResult validateOrder(String merchantTransId, Double amount) {
        // Example: look up your order from the database
        // Order order = orderRepository.findById(Long.parseLong(merchantTransId)).orElse(null);

        // If order doesn't exist:
        // if (order == null) {
        //     return ClickOrderResult.error(ClickErrorCode.ORDER_NOT_FOUND);
        // }

        // If amounts don't match:
        // if (Math.abs(order.getAmount() - amount) > 0.01) {
        //     return ClickOrderResult.error(ClickErrorCode.INCORRECT_AMOUNT);
        // }

        // If order was already paid:
        // if (order.isPaid()) {
        //     return ClickOrderResult.error(ClickErrorCode.ALREADY_PAID);
        // }

        // If everything is fine:
        // return ClickOrderResult.ok(order.getId());

        // --- PLACEHOLDER (replace with your real logic) ---
        return ClickOrderResult.ok(Long.parseLong(merchantTransId));
    }

    /**
     * Called when Click sends a PREPARE request (Action = 0).
     * This means: "A user wants to pay for this order. Please reserve it."
     *
     * You should:
     * - Mark the order as "waiting for payment" in your database
     * - Save the click_trans_id for future reference
     * - Return the merchant_prepare_id (usually your order ID or transaction ID)
     *
     * @param request - contains click_trans_id, merchant_trans_id, amount, etc.
     * @return ClickOrderResult with merchant_prepare_id
     */
    @Override
    public ClickOrderResult prepareOrder(ClickPrepareRequest request) {
        // Example:
        // Order order = orderRepository.findById(Long.parseLong(request.getMerchantTransId())).get();
        // order.setStatus("WAITING_PAYMENT");
        // order.setClickTransId(request.getClickTransId());
        // orderRepository.save(order);
        // return ClickOrderResult.ok(order.getId());

        // --- PLACEHOLDER (replace with your real logic) ---
        return ClickOrderResult.ok(Long.parseLong(request.getMerchantTransId()));
    }

    /**
     * Called when Click sends a COMPLETE request (Action = 1).
     * This means either:
     *   - Payment was SUCCESSFUL (request.getError() == 0) -> deliver the product/service
     *   - Payment was CANCELLED (request.getError() < 0) -> cancel the reservation
     *
     * You should:
     * - Check request.getError() to know if it succeeded or failed
     * - Update your order status in the database
     * - Return confirmed() on success, or error() on failure
     *
     * @param request - contains click_trans_id, merchant_trans_id, merchant_prepare_id, error, etc.
     * @return ClickOrderResult with merchant_confirm_id
     */
    @Override
    public ClickOrderResult completeOrder(ClickCompleteRequest request) {
        // Example:
        // Order order = orderRepository.findById(Long.parseLong(request.getMerchantTransId())).get();
        //
        // if (request.getError() != null && request.getError() < 0) {
        //     // Payment was cancelled by Click
        //     order.setStatus("CANCELLED");
        //     orderRepository.save(order);
        //     return ClickOrderResult.error(ClickErrorCode.TRANSACTION_CANCELLED);
        // }
        //
        // // Payment was successful!
        // order.setStatus("PAID");
        // orderRepository.save(order);
        // return ClickOrderResult.confirmed(order.getId(), order.getId());

        // --- PLACEHOLDER (replace with your real logic) ---
        return ClickOrderResult.confirmed(
                request.getMerchantPrepareId(),
                request.getMerchantPrepareId()
        );
    }
}

Step 2: That's it!

Once you create this @Service bean, the library automatically:

  • Registers POST /api/click/prepare endpoint
  • Registers POST /api/click/complete endpoint
  • Validates the signature (MD5 hash) on every request from Click
  • Calls your validateOrder(), prepareOrder(), and completeOrder() methods
  • Returns the correct JSON response to Click

You give these URLs to Click when setting up your merchant account:

  • Prepare URL: https://your-domain.com/api/click/prepare
  • Complete URL: https://your-domain.com/api/click/complete

Part 2: Call Click's API from your code (Merchant API) — Optional

If you need to create invoices, manage card tokens, or cancel payments, inject ClickMerchantApiService:

import integration.payment.clickintegration.model.merchant.*;
import integration.payment.clickintegration.service.ClickMerchantApiService;
import org.springframework.stereotype.Service;

@Service
public class MyPaymentService {

    private final ClickMerchantApiService clickMerchantApi;

    public MyPaymentService(ClickMerchantApiService clickMerchantApi) {
        this.clickMerchantApi = clickMerchantApi;
    }

    // =====================================================
    // EXAMPLE 1: Create an invoice (sends SMS to the user)
    // =====================================================
    public void sendPaymentRequest() {
        CreateInvoiceResponse response = clickMerchantApi.createInvoice(
                "998901234567",   // user's phone number (with country code)
                50000.0,          // amount in UZS
                "order-123"       // your order ID
        );

        if (response.getErrorCode() == 0) {
            System.out.println("Invoice created! ID: " + response.getInvoiceId());
            // Save response.getInvoiceId() to your database
        } else {
            System.out.println("Error: " + response.getErrorNote());
        }
    }

    // =====================================================
    // EXAMPLE 2: Check if an invoice was paid
    // =====================================================
    public void checkPayment(Long invoiceId) {
        CheckInvoiceResponse response = clickMerchantApi.checkInvoice(invoiceId);

        System.out.println("Invoice status: " + response.getInvoiceStatus());
        // Status > 0  means paid
        // Status == -99 means rejected
        // Status < 0  means error
    }

    // =====================================================
    // EXAMPLE 3: Cancel (reverse) a payment
    // =====================================================
    public void cancelPayment(Long paymentId) {
        CancelPaymentResponse response = clickMerchantApi.cancelPayment(paymentId);

        if (response.getErrorCode() == 0) {
            System.out.println("Payment cancelled successfully!");
        } else {
            System.out.println("Cancel failed: " + response.getErrorNote());
        }
    }

    // =====================================================
    // EXAMPLE 4: Full card token flow
    // =====================================================
    public void payWithCard() {
        // Step 1: Create a card token (user will receive an SMS code)
        CreateCardTokenResponse tokenResponse = clickMerchantApi.createCardToken(
                "8600123456789012",  // card number
                "0625",             // expiry date (MMYY)
                0                   // 0 = permanent token, 1 = one-time use
        );

        if (tokenResponse.getErrorCode() != 0) {
            System.out.println("Error creating token: " + tokenResponse.getErrorNote());
            return;
        }

        String cardToken = tokenResponse.getCardToken();

        // Step 2: Verify the card token with SMS code from the user
        VerifyCardTokenResponse verifyResponse = clickMerchantApi.verifyCardToken(
                cardToken,
                "123456"  // SMS code the user received
        );

        if (verifyResponse.getErrorCode() != 0) {
            System.out.println("Verification failed: " + verifyResponse.getErrorNote());
            return;
        }

        // Step 3: Make a payment using the verified card token
        PaymentWithTokenResponse payResponse = clickMerchantApi.paymentWithToken(
                cardToken,
                50000.0,       // amount in UZS
                "order-456",   // your order ID
                "user-789"     // user/contract identifier in your system
        );

        if (payResponse.getErrorCode() == 0) {
            System.out.println("Payment successful! ID: " + payResponse.getPaymentId());
        } else {
            System.out.println("Payment failed: " + payResponse.getErrorNote());
        }

        // Step 4 (optional): Delete the card token when no longer needed
        DeleteCardTokenResponse deleteResponse = clickMerchantApi.deleteCardToken(cardToken);
        System.out.println("Token deleted: " + (deleteResponse.getErrorCode() == 0));
    }
}

Understanding the Payment Flow

Here is the complete flow of how a Click payment works with this library:

┌──────────┐         ┌───────────┐         ┌──────────────────┐
│   User   │         │   CLICK   │         │   Your Server    │
│          │         │  System   │         │  (this library)  │
└────┬─────┘         └─────┬─────┘         └────────┬─────────┘
     │                     │                        │
     │  1. User pays       │                        │
     │  via Click app      │                        │
     │ ──────────────────> │                        │
     │                     │                        │
     │                     │  2. POST /prepare      │
     │                     │ ────────────────────>  │
     │                     │                        │  Library verifies signature
     │                     │                        │  Library calls YOUR validateOrder()
     │                     │                        │  Library calls YOUR prepareOrder()
     │                     │  3. Response (ok/error) │
     │                     │ <────────────────────  │
     │                     │                        │
     │                     │  4. POST /complete     │
     │                     │ ────────────────────>  │
     │                     │                        │  Library verifies signature
     │                     │                        │  Library calls YOUR validateOrder()
     │                     │                        │  Library calls YOUR completeOrder()
     │                     │  5. Response (ok/error) │
     │                     │ <────────────────────  │
     │                     │                        │
     │  6. "Payment done!" │                        │
     │ <────────────────── │                        │
     │                     │                        │

All Request and Response Models

Every Click API field is mapped to a Java class. You can use these for your own logic, database mapping, logging, etc.

SHOP-API Models (incoming from Click)

These are in the package integration.payment.clickintegration.model.shop:

Class Purpose Key Fields
ClickPrepareRequest Click sends this to prepare a payment clickTransId, serviceId, merchantTransId, amount, action (0), signString, signTime
ClickPrepareResponse Your server returns this clickTransId, merchantTransId, merchantPrepareId, error, errorNote
ClickCompleteRequest Click sends this to complete a payment clickTransId, serviceId, merchantTransId, merchantPrepareId, amount, action (1), error, signString, signTime
ClickCompleteResponse Your server returns this clickTransId, merchantTransId, merchantPrepareId, merchantConfirmId, error, errorNote

Merchant-API Models (outgoing to Click)

These are in the package integration.payment.clickintegration.model.merchant:

Class Purpose
CreateInvoiceRequest Send an invoice to a user's phone
CreateInvoiceResponse Response with invoice ID
CheckInvoiceResponse Invoice status check result
CancelPaymentResponse Payment cancellation result
CreateCardTokenRequest Request to tokenize a card
CreateCardTokenResponse Response with card token
VerifyCardTokenRequest Verify card token with SMS code
VerifyCardTokenResponse Verification result
PaymentWithTokenRequest Pay using a saved card token
PaymentWithTokenResponse Payment result with status
DeleteCardTokenResponse Card token deletion result

Error Codes

The ClickErrorCode enum (in integration.payment.clickintegration.model) contains all possible errors:

Code Name What it means
0 SUCCESS Everything is fine
-1 SIGN_CHECK_FAILED The signature in the request is invalid (possible tampering)
-2 INCORRECT_AMOUNT The amount doesn't match your order
-3 ACTION_NOT_FOUND Invalid action value (must be 0 for prepare, 1 for complete)
-4 ALREADY_PAID This order was already paid
-5 ORDER_NOT_FOUND The order ID doesn't exist in your system
-6 TRANSACTION_NOT_FOUND The transaction doesn't exist
-7 BAD_REQUEST General update error
-8 ERROR_IN_REQUEST_FROM_CLICK Missing or invalid fields in Click's request
-9 TRANSACTION_CANCELLED The payment was cancelled

Use these in your ClickOrderService implementation:

// Order not found
return ClickOrderResult.error(ClickErrorCode.ORDER_NOT_FOUND);

// Amount mismatch
return ClickOrderResult.error(ClickErrorCode.INCORRECT_AMOUNT);

// Custom error message
return ClickOrderResult.error(ClickErrorCode.ORDER_NOT_FOUND, "Order #123 was deleted");

Convenience Methods on ClickOrderResult

Method When to use
ClickOrderResult.ok(merchantPrepareId) Prepare succeeded — pass your order/transaction ID
ClickOrderResult.confirmed(merchantPrepareId, merchantConfirmId) Complete succeeded — pass both IDs
ClickOrderResult.error(ClickErrorCode) Something went wrong — pass the error code
ClickOrderResult.error(ClickErrorCode, "custom message") Something went wrong — with a custom error message

Project Structure

click-integration-spring-boot-starter/
│
├── config/
│   ├── ClickProperties.java            — Reads your click.* settings from application.yml
│   └── ClickAutoConfiguration.java     — Auto-creates all beans when the library is on the classpath
│
├── controller/
│   └── ClickShopApiController.java     — The /prepare and /complete REST endpoints
│
├── model/
│   ├── ClickErrorCode.java             — All error codes as a Java enum
│   │
│   ├── shop/                            — Models for SHOP-API (Click calls you)
│   │   ├── ClickPrepareRequest.java
│   │   ├── ClickPrepareResponse.java
│   │   ├── ClickCompleteRequest.java
│   │   └── ClickCompleteResponse.java
│   │
│   └── merchant/                        — Models for Merchant-API (you call Click)
│       ├── CreateInvoiceRequest.java
│       ├── CreateInvoiceResponse.java
│       ├── CheckInvoiceResponse.java
│       ├── CancelPaymentResponse.java
│       ├── CreateCardTokenRequest.java
│       ├── CreateCardTokenResponse.java
│       ├── VerifyCardTokenRequest.java
│       ├── VerifyCardTokenResponse.java
│       ├── PaymentWithTokenRequest.java
│       ├── PaymentWithTokenResponse.java
│       └── DeleteCardTokenResponse.java
│
├── service/
│   ├── ClickOrderService.java          — Interface YOU implement (your business logic)
│   ├── ClickOrderResult.java           — Result object you return from your implementation
│   ├── ClickShopApiService.java        — Handles incoming webhooks (signature check + your logic)
│   └── ClickMerchantApiService.java    — Makes outgoing HTTP calls to Click's API
│
└── util/
    └── ClickSignatureUtil.java         — MD5 and SHA-1 hash generation for signatures

FAQ

Q: Do I need to create the /prepare and /complete endpoints myself? No. The library creates them automatically. Just implement ClickOrderService and mark it with @Service.

Q: Where do I get the merchant credentials? When you sign a contract with Click as a merchant, they provide you with merchant_id, service_id, merchant_user_id, and secret_key.

Q: What if I only want to use the Merchant API (create invoices, etc.) and don't need webhooks? That works too. ClickMerchantApiService is always available as long as you configure click.secret-key in your application.yml. The webhook endpoints only activate when you provide a ClickOrderService bean.

Q: Can I change the webhook URL path? Yes. Set click.shop-api.base-path in your application.yml. For example, click.shop-api.base-path=/payments/click will make the endpoints /payments/click/prepare and /payments/click/complete.

Q: What if I already have an OkHttpClient bean? The library creates its own OkHttpClient bean (clickOkHttpClient) only if you don't already have one (@ConditionalOnMissingBean). If you have a custom OkHttpClient bean, the library will use yours.

Q: How does signature verification work? Click sends a sign_string field with every request. The library computes the expected hash using your secret_key and the request parameters, then compares it. If they don't match, the request is rejected with error code -1. You don't need to do anything — this happens automatically.

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages