DEV Community

Roberto Luna
Roberto Luna

Posted on

Automatic Google Calendar Token Alerts & Robust Contract API Refactor in VibeCoding

Automatic Google Calendar Token Alerts & Robust Contract API Refactor in VibeCoding

TL;DR: I added a cron‑driven email alert that warns us 24 h before a Google Calendar refresh token expires, and I refactored the contracts controllers to return clear validation errors instead of generic 500s. The result is fewer silent failures and better observability for the whole CRM.


The Problem

Two separate pain points kept popping up in production:

  1. Google Calendar token expiry – Our booking integration silently stopped sending invites once the refresh token reached its 7‑day grace period. The only symptom was “no invites” and we had no way to know the token was about to die.
  2. Contracts endpoints returning 500 – Calls to /contracts/print, /contracts/create, and /contracts/update would sometimes throw an unhandled QueryFailedError. The API responded with a generic 500, leaving the front‑end with “Something went wrong” and no clue what to fix.

Both issues were “real‑world” bugs that weren’t covered by any test, so they slipped through code review.


What I Tried First

Google Calendar

My first attempt was to catch the invalid_grant error inside the booking service and immediately re‑authenticate. That worked for the moment the error happened, but it didn’t give us any warning before the token became unusable, and the retry logic added latency to the booking flow.

Contracts Errors

I tried to wrap each controller method in a try/catch and manually throw new BadRequestException(err.message). Unfortunately the NestJS exception filter still turned some DB constraint violations into 500 because the original error type was lost in the catch block.

Both approaches felt like band‑aid patches. I needed a systematic solution that would:

  • Proactively notify about token expiry.
  • Surface validation errors directly from the service layer.
  • Be covered by automated tests so future refactors don’t re‑introduce the bugs.

The Implementation

1. Email Cron for Token Expiration

I created a new cron job in apps/api/src/email/email.cron.ts that runs every hour, checks the stored token expiration timestamp, and triggers an alert email when the expiry is within 24 h.

// apps/api/src/email/email.cron.ts
import {
  sendPropertyVacant,
  sendDailySummary,
  sendOwnerMonthlyReport,
  sendGoogleTokenExpiring,   // ← new import
} from "./email.service.js";
import { runBookingReminders } from "../booking/booking.reminders.js";

export const scheduleEmails = () => {
  // existing schedules …
  Cron("0 * * * *", async () => {
    await sendGoogleTokenExpiring();
  });
};
Enter fullscreen mode Exit fullscreen mode

The service implementation lives in email.service.ts:

// apps/api/src/email/email.service.ts
import { getRepository } from "typeorm";
import { GoogleToken } from "../auth/google-token.entity.js";
import { sendMail } from "./mailer.js";

export async function sendGoogleTokenExpiring() {
  // ─── 10. Token de Google Calendar por vencer ───────────────────────
  // Google expira el refresh_token a los 7 días sin uso. We query the
  // token table for any entry whose `expires_at` is < now() + 24h.
  const repo = getRepository(GoogleToken);
  const soonToExpire = await repo.find({
    where: qb => qb.where("expires_at <= NOW() + INTERVAL '24 hour'"),
  });

  for (const token of soonToExpire) {
    await sendMail({
      to: token.ownerEmail,
      subject: "⚠️ Google Calendar token expiring soon",
      html: `
        <p>Hello,</p>
        <p>Your Google Calendar integration token will expire on
        <strong>${token.expires_at.toISOString()}</strong>. Please
        re‑authenticate <a href="${process.env.APP_URL}/settings/google">here</a>
        to avoid booking interruptions.</p>
      `,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Why a cron?

The token expiration date is stored in the DB when the OAuth flow completes. A cron is cheap (no extra infra) and gives us deterministic timing without adding latency to the booking request path.

2. Explicit Error Handling in Contracts Controllers

I cleaned up the imports (the previous typo UseGuard broke the guard pipeline) and, more importantly, introduced a helper handleDbError that maps known DB errors to HTTP exceptions.

// apps/api/src/contracts/contracts.controller.ts
import {
  Body, Controller, Delete, Get, Param, Patch, Post, Query,
  UseGuards, Res, BadRequestException, NotFoundException,
} from "@nestjs/common";
import { ContractsService } from "./contracts.service.js";
import { handleDbError } from "./error.utils.js";

@Controller("contracts")
export class ContractsController {
  constructor(private readonly contractsService: ContractsService) {}

  @Post()
  async create(@Body() dto: CreateContractDto, @Res() res) {
    try {
      const result = await this.contractsService.create(dto);
      return res.status(201).json(result);
    } catch (err) {
      throw handleDbError(err);
    }
  }

  // similar pattern for print() and update()
}
Enter fullscreen mode Exit fullscreen mode

The new utility lives in error.utils.ts:

// apps/api/src/contracts/error.utils.ts
import {
  BadRequestException,
  NotFoundException,
  InternalServerErrorException,
} from "@nestjs/common";

export function handleDbError(err: any) {
  // Unique constraint violation
  if (err.code === "23505") {
    return new BadRequestException("Duplicate contract identifier");
  }
  // Foreign key violation
  if (err.code === "23503") {
    return new BadRequestException("Referenced entity does not exist");
  }
  // Record not found (our service throws a custom NotFoundError)
  if (err.name === "NotFoundError") {
    return new NotFoundException(err.message);
  }
  // Fallback
  return new InternalServerErrorException("Unexpected server error");
}
Enter fullscreen mode Exit fullscreen mode

Now the API returns 400 with a human‑readable message instead of a 500 stack trace.

3. Test Coverage Boost

To guard against regression, I added two test suites:

  • contract-templates.test.ts – validates that contract templates are correctly scoped by type (renta vs venta) and that the new configuration endpoint respects the moved location.
  • sale-contracts.test.ts – exercises the CRUD flow, pagination filters, and the new error handling.

A snippet from the sale‑contracts


Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.

Repo: zaerohell/VS · 2026-08-17

#playadev #buildinpublic

Top comments (0)