MohammedMohammedMohammedAnas K V

Initializing
0%
Press?for keyboard shortcuts
Back to Blog
Payments

Building Real-Time Payment Device Sync with Geidea: Click to Sale in 3 Seconds

How I built a WebSocket-driven POS system that pushes transactions to Geidea payment terminals and auto-saves sales on customer card tap.

February 15, 2025
15 min read
GeideaWebSocketAngularNestJSPOSPayments

The Vision: A POS That Just Works

Picture this: cashier adds products to cart → clicks "Card Payment" → terminal shows the amount → customer taps card → sale auto-saves in our software, receipt prints, inventory updates. All in under 3 seconds.

This is what I built for CAAD ERP with Geidea payment terminals.

The Core Challenge

Traditional POS integration takes 5-10 seconds with polling:

  1. 1Cashier clicks "Charge"
  2. 2Software sends request to payment gateway
  3. 3Gateway processes card
  4. 4Software polls gateway for status
  5. 5Software receives "approved"
  6. 6Sale saves

My solution: WebSocket-driven real-time sync that completes in 2-3 seconds.

Architecture Overview

typescript
[POS Frontend] --WebSocket--> [NestJS Backend] --WebSocket--> [Geidea Terminal]
       ↓                            ↓
   UI Update                  Sale Record
   Receipt Print              Inventory Update

Backend: WebSocket Gateway

typescript
@WebSocketGateway({ cors: { origin: '*' } })
export class PaymentGateway {
  @WebSocketServer()
  server: Server;

  private geideaWS: WebSocket;
  private pendingTransactions = new Map<string, Subject<any>>();

  async handleConnection(client: Socket) {
    // Authenticate connection
    const token = client.handshake.auth.token;
    const user = await this.validateToken(token);
    client.data.user = user;
  }

  @SubscribeMessage('process_payment')
  async processPayment(client: Socket, payload: any) {
    const transactionId = uuid();
    const subject = new Subject();
    this.pendingTransactions.set(transactionId, subject);

    // Set 60 second timeout for customer to tap card
    const timeout = setTimeout(() => {
      this.pendingTransactions.delete(transactionId);
      subject.error(new Error('Payment timeout - customer did not tap card'));
    }, 60000);

    this.pendingTransactions.get(transactionId)!
      .pipe(take(1))
      .subscribe({
        next: (result) => {
          clearTimeout(timeout);
          resolve(result);
        },
        error: (err) => {
          clearTimeout(timeout);
          reject(err);
        }
      });

    // Send to Geidea terminal
    this.geideaWS.send(JSON.stringify({
      action: 'PUSH_TRANSACTION',
      transactionId,
      amount: payload.amount,
      currency: 'AED'
    }));
  }

  private handleGeideaMessage(msg: any) {
    const subject = this.pendingTransactions.get(msg.transactionId);
    if (!subject) return;

    switch (msg.status) {
      case 'APPROVED':
        subject.next({
          success: true,
          transactionId: msg.geideaTransactionId,
          authCode: msg.authCode,
          cardLast4: msg.cardLast4
        });
        subject.complete();
        this.pendingTransactions.delete(msg.transactionId);
        break;

      case 'DECLINED':
        subject.error(new Error(`Card declined - ${msg.reason}`));
        this.pendingTransactions.delete(msg.transactionId);
        break;

      case 'CANCELLED':
        subject.error(new Error('Customer cancelled'));
        this.pendingTransactions.delete(msg.transactionId);
        break;
    }
  }
}

Sale Transaction Endpoint

typescript
@Post('sales/process')
@UseGuards(JwtAuthGuard)
async processSale(@Body() saleData: CreateSaleDto, @Req() req) {
  const saleId = uuid();

  const sale = await this.saleModel.create({
    _id: saleId,
    ...saleData,
    status: 'PENDING_PAYMENT',
    cashier: req.user.id,
    timestamp: new Date()
  });

  try {
    const paymentResult = await this.paymentGateway.processPayment({
      amount: saleData.total,
      items: saleData.items
    });

    const completedSale = await this.saleModel.findByIdAndUpdate(
      saleId,
      {
        $set: {
          status: 'COMPLETED',
          paymentResult,
          completedAt: new Date()
        }
      },
      { new: true }
    );

    // Async: print receipt, update inventory
    this.printService.printReceipt(completedSale)
      .catch(err => this.logger.error('Print failed', err));

    this.inventoryService.decrementStock(saleData.items)
      .catch(err => this.logger.error('Stock update failed', err));

    return completedSale;

  } catch (paymentError) {
    await this.saleModel.findByIdAndUpdate(saleId, {
      $set: {
        status: 'FAILED',
        failureReason: paymentError.message,
        failedAt: new Date()
      }
    });
    throw paymentError;
  }
}

WebSocket Reconnection Logic

Terminal connections drop. Handle it gracefully:

typescript
class TerminalConnection {
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;

  setupReconnection() {
    this.ws.on('close', () => {
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);

        setTimeout(() => {
          this.reconnectAttempts++;
          this.connect();
        }, delay);
      } else {
        this.alertOps('Payment terminal offline for 10+ minutes');
      }
    });

    this.ws.on('open', () => {
      this.reconnectAttempts = 0;
    });
  }
}

Real Production Numbers

After deploying this across 5 retail stores:

  • Average sale completion: 2.8 seconds
  • Customer satisfaction: +40% (vs old 8-second wait)
  • Cashier training time: 30 minutes (vs 2 hours)
  • Failed transactions: 0.3% (industry average is 2%)

Wrapping Up

Real-time POS integration is the difference between a frustrating checkout and a delightful one. Customers notice the 3-second sale vs the 8-second sale.

Key takeaways:

  • WebSockets for real-time payment sync
  • Pending transaction map for request/response correlation
  • Graceful reconnection with exponential backoff
  • Automatic inventory + receipt printing after payment

This pattern is now in production serving 20K+ users across UAE.

Technologies

GeideaWebSocketAngularNestJSPOSPayments
Share