Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9ee6cba
feat: migrate Veo3 video provider from kie.ai to EvoLink
giladresisi Aug 24, 2026
83c33dc
faet: rc
nevo-david Aug 26, 2026
e2734f4
Merge remote-tracking branch 'origin/main' into feat/rc
nevo-david Aug 26, 2026
6ca278a
feat: tc
nevo-david Aug 27, 2026
1450d87
chore: add EVOLINK_API_KEY to .env.example
Copilot Aug 27, 2026
6d661f7
feat(organization-repository): enhance organization filtering and use…
egelhaus Aug 28, 2026
d2a7b50
feat(pr-template): enhance QA section requirements and clarity in PR …
egelhaus Aug 29, 2026
a1b9456
feat(pr-template): improve clarity and detail in PR description guide…
egelhaus Aug 29, 2026
295fbc0
feat: change evolink model
nevo-david Aug 30, 2026
e436a1c
Merge remote-tracking branch 'origin/feat/evolink-video-provider' int…
nevo-david Aug 30, 2026
8417aa6
Merge remote-tracking branch 'origin/main' into feat/rc
nevo-david Aug 30, 2026
2d9adb8
feat: stripe changes
nevo-david Aug 30, 2026
60ffa4d
feat: fix X
nevo-david Aug 30, 2026
e1baea0
feat: dynamic path
nevo-david Sep 1, 2026
0d8d0f2
feat: localhost for mcp
nevo-david Sep 1, 2026
94778db
feat: remove filter
nevo-david Sep 1, 2026
50b171e
feat: mcp changes
nevo-david Sep 1, 2026
6b6ca12
feat: evolink
nevo-david Sep 1, 2026
c98ea04
feat: fix linkedin dto
nevo-david Sep 1, 2026
8833276
video status tool
nevo-david Sep 1, 2026
47580d1
feat: fix video
nevo-david Sep 1, 2026
342f700
feat: failures
nevo-david Sep 1, 2026
eae605d
feat(orchestrator): post workflow v1.1.1 retries never-started activi…
giladresisi Aug 31, 2026
b2e438f
feat: reminders
nevo-david Sep 2, 2026
db1a49e
Merge pull request #1996 from gitroomhq/feat/heartbeat-timeout-retry
egelhaus Sep 2, 2026
bfbba88
chore(sync): merge latest upstream main (Post Workflow v1.1.1, Revenu…
JOY Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ OPENAI_IMAGE_MODEL="chatgpt-image-latest"
OPENAI_OAUTH_CLIENT_ID="sample_openai_oauth_client_id"
# OpenAI App domain verification challenge string (optional)
OPENAI_APP_CHALLANGE="sample_openai_app_challenge_token"
# EvoLink API key for Seedance AI video generation (https://evolink.ai)
EVOLINK_API_KEY="sample_evolink_api_key"
# Tavily AI Search API Key for real-time web grounding in agent workflows (optional)
TAVILY_API_KEY="tvly-sample_tavily_api_key"
# Fal.ai API Key for image and video rendering generation (optional)
Expand Down Expand Up @@ -406,7 +408,7 @@ MIGRATE_PROVIDERS="tiktok:tiktok-business"
NX_ADD_PLUGINS="false"

# ==============================================================================
# 14. Stripe Billing & Subscription Plans
# 14. Stripe & In-App Purchase Billing (RevenueCat / Stripe)
# ==============================================================================
# Platform fee percentage (0.05 = 5%)
FEE_AMOUNT="0.05"
Expand All @@ -420,6 +422,12 @@ STRIPE_SIGNING_KEY="whsec_sample_stripe_webhook_signing_secret"
STRIPE_SIGNING_KEY_CONNECT="whsec_sample_stripe_connect_signing_secret"
# Stripe coupon / promotional discount ID
STRIPE_DISCOUNT_ID="sample_stripe_coupon_discount_id"
# RevenueCat Secret Key (App Store & Google Play subscriptions)
REVENUECAT_SECRET_KEY="sample_revenuecat_secret_key"
# RevenueCat Webhook Authorization Header Secret
REVENUECAT_WEBHOOK_SECRET="sample_revenuecat_webhook_secret"
# Reject sandbox in-app purchases in production mode (true/false)
IN_APP_PURCHASE_REJECT_SANDBOX="false"

# ==============================================================================
# 15. Telemetry, Observability & Analytics
Expand Down
14 changes: 10 additions & 4 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# What kind of change does this PR introduce?

<!-- [ ] Bug fix | [ ] Feature | [ ] Architecture & Docs | [ ] Refactor | [ ] CI/CD -->
<!-- Name the change type, scope/component, and summary of changes. -->

# Why was this change needed?

Expand All @@ -18,13 +19,18 @@

<!--
Write the steps here, replacing this whole comment. Leaving it as is, or writing
"N/A" / "TBD" / an empty checkbox, counts as no QA at all.
"N/A" / "TBD" / a bare empty checkbox as the whole section, counts as no QA at all.

Write real steps a reviewer can follow without asking you anything: setup, action,
expected result. Keep them as numbered checkboxes so a reviewer can tick them off -
the numbering is what the review board extracts, the checkbox is for the reviewer.
Steps inside a fenced code block are ignored, so keep them as plain lines.

Example of what it should look like:

1. Link a webhook endpoint pointing at http://localhost:9999 (nothing listening)
2. Approve an application to trigger a delivery
3. Delivery should show 4 attempts, roughly 1m / 5m / 30m apart, then stop
1. [ ] Link a webhook endpoint pointing at http://localhost:9999 (nothing listening)
2. [ ] Approve an application to trigger a delivery
3. [ ] Delivery should show 4 attempts, roughly 1m / 5m / 30m apart, then stop
-->

# Checklist:
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ const useCommunity = () => {
- Whenever you generate a PR, PR description, or similar, **always** follow the PR Template (.github/PULL_REQUEST_TEMPLATE.md)
- Every PR description **must** contain a `# QA` section with real, numbered steps a reviewer can follow to verify the change (setup, action, expected result), written so they can be run without asking the author anything. This is not optional and applies to humans and agents alike, including one-line fixes. The section is extracted verbatim and shown on the review board, so:
- Use the exact heading `# QA` (`# Testing`, `# Test plan`, `# How to test`, `# How to verify`, `# Verification`, `# Steps to test` and `# Manual testing` are also recognised, but prefer `# QA`). The whole heading must match, so something like `## Testing philosophy` is not picked up.
- Never leave the template placeholder in place, and never write `N/A`, `TBD`, `todo`, `none` or an empty checkbox there - those all count as no QA at all and the board will show the PR as missing testing notes.
- Steps inside a fenced code block are ignored, so keep them as plain numbered lines.
- Never leave the template placeholder in place, and never write `N/A`, `TBD`, `todo`, `none` or a bare empty checkbox as the whole section - those all count as no QA at all and the board will show the PR as missing testing notes.
- Steps inside a fenced code block are ignored, so keep them as plain numbered lines. Write each step as a numbered checkbox (`1. [ ] step`) so a reviewer can tick it off while working through it - the numbering is what the board extracts, the checkbox is for the reviewer.
- Every PR description **must** answer `# What kind of change does this PR introduce?` with actual detail, not just a category. `Bug fix.` / `Feature.` on its own is not acceptable. State the type, the area it touches (backend, frontend, orchestrator, a specific provider or screen), and in one to three sentences what concretely changed and where - the key function, endpoint, file or field - plus what deliberately stayed the same. A reader should understand the change from this section alone, without opening the diff.
- Avoid as much as possible creating new files with pure logic of algorithms, it's usually wrong
- When you write code, make sure that what you add looks like something similar somewhere else in the code, don't make weird patterns
- When you finished running, run another agents that matches the new code with the existing system code, to see that it looks similar and is not a weird pattern.
Expand Down
10 changes: 9 additions & 1 deletion apps/backend/src/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { AuthController } from '@gitroom/backend/api/routes/auth.controller';
import { AuthService } from '@gitroom/backend/services/auth/auth.service';
import { UsersController } from '@gitroom/backend/api/routes/users.controller';
import { AuthMiddleware } from '@gitroom/backend/services/auth/auth.middleware';
import { StripeController } from '@gitroom/backend/api/routes/stripe.controller';
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
import { PaymentController } from '@gitroom/backend/api/routes/payment.controller';
import { PaymentService } from '@gitroom/nestjs-libraries/services/payment/payment.service';
import { PaymentProviderManager } from '@gitroom/nestjs-libraries/services/payment/payment.provider.manager';
import { RevenueCatProvider } from '@gitroom/nestjs-libraries/services/payment/providers/revenuecat.provider';
import { AnalyticsController } from '@gitroom/backend/api/routes/analytics.controller';
import { PoliciesGuard } from '@gitroom/backend/services/auth/permissions/permissions.guard';
import { PermissionsService } from '@gitroom/backend/services/auth/permissions/permissions.service';
Expand Down Expand Up @@ -48,6 +51,7 @@ import { AppleProvider } from '@gitroom/backend/services/auth/providers/apple.pr
import { FarcasterProvider } from '@gitroom/backend/services/auth/providers/farcaster.provider';
import { WalletProvider } from '@gitroom/backend/services/auth/providers/wallet.provider';
import { OauthProvider } from '@gitroom/backend/services/auth/providers/oauth.provider';
import { StripeController } from '@gitroom/backend/api/routes/stripe.controller';

const authenticatedController = [
UsersController,
Expand Down Expand Up @@ -76,6 +80,7 @@ const authenticatedController = [
? [RootController, OAuthController]
: [
RootController,
PaymentController,
StripeController,
AuthController,
PublicController,
Expand All @@ -88,6 +93,9 @@ const authenticatedController = [
providers: [
AuthService,
StripeService,
PaymentService,
PaymentProviderManager,
RevenueCatProvider,
OpenaiService,
ExtractContentService,
AuthMiddleware,
Expand Down
92 changes: 63 additions & 29 deletions apps/backend/src/api/routes/billing.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Body, Controller, Get, HttpException, Param, Post, Req } from '@nestjs/common';
import {
Body,
Controller,
Get,
HttpException,
Param,
Post,
Req,
} from '@nestjs/common';
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { Organization, User } from '@prisma/client';
import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto';
Expand All @@ -11,17 +18,25 @@ import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/n
import { Request } from 'express';
import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service';
import { PaymentService } from '@gitroom/nestjs-libraries/services/payment/payment.service';
import { BillingSyncDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.sync.dto';

@ApiTags('Billing')
@Controller('/billing')
export class BillingController {
constructor(
private _subscriptionService: SubscriptionService,
private _stripeService: StripeService,
private _notificationService: NotificationService,
private _usersService: UsersService
private _usersService: UsersService,
private _paymentService: PaymentService
) {}

// Billing routes are the web platform; the org's own provider (or the web
// default when it has none) handles the action.
private provider(org: Organization) {
return this._paymentService.getProviderForOrganization(org.id, 'web');
}

private async assertNoOtherSubscribedAccount(user: User) {
const other = await this._usersService.getUserWithActiveSubscriptionByEmail(
user.email,
Expand All @@ -36,28 +51,29 @@ export class BillingController {
@Param('id') body: string
) {
return {
status: await this._stripeService.checkSubscription(org.id, body),
status: await (await this.provider(org)).checkSubscription(org.id, body),
};
}

@Get('/check-discount')
async checkDiscount(@GetOrgFromRequest() org: Organization) {
return {
offerCoupon: !(await this._stripeService.checkDiscount(org.paymentId))
offerCoupon: !(await (await this.provider(org)).checkDiscount(org))
? false
: AuthService.signJWT({ discount: true }),
};
}

@Post('/apply-discount')
async applyDiscount(@GetOrgFromRequest() org: Organization) {
await this._stripeService.applyDiscount(org.paymentId);
await (await this.provider(org)).applyDiscount(org);
}

@Post('/finish-trial')
async finishTrial(@GetOrgFromRequest() org: Organization) {
const provider = await this.provider(org);
try {
await this._stripeService.finishTrial(org.paymentId);
await provider.finishTrial(org);
} catch (err) {}
return {
finish: true,
Expand All @@ -83,7 +99,7 @@ export class BillingController {
}

const uniqueId = req?.cookies?.track;
return this._stripeService.embedded(
return (await this.provider(org)).embedded(
uniqueId,
org.id,
user.id,
Expand All @@ -104,7 +120,7 @@ export class BillingController {
}

const uniqueId = req?.cookies?.track;
return this._stripeService.subscribe(
return (await this.provider(org)).subscribe(
uniqueId,
org.id,
user.id,
Expand All @@ -113,20 +129,38 @@ export class BillingController {
);
}

@Post('/sync')
async sync(
@GetOrgFromRequest() org: Organization,
@GetUserFromRequest() user: User,
@Body() body: BillingSyncDto
) {
if (await this.assertNoOtherSubscribedAccount(user)) {
return { blocked: true };
}
await this._paymentService.assertCanUseProvider(org.id, body.provider);

try {
return await this._paymentService.syncSubscription(body.provider, org.id);
} catch (e) {
if (e instanceof HttpException) {
throw e;
}
throw new HttpException((e as Error)?.message || 'Sync failed', 400);
}
}

@Get('/portal')
async modifyPayment(@GetOrgFromRequest() org: Organization) {
const customer = await this._stripeService.getCustomerByOrganizationId(
org.id
);
const { url } = await this._stripeService.createBillingPortalLink(customer);
const { url } = await (await this.provider(org)).portalLink(org.id);
return {
portal: url,
};
}

@Get('/')
getCurrentBilling(@GetOrgFromRequest() org: Organization) {
return this._subscriptionService.getSubscriptionByOrganizationId(org.id);
return this._paymentService.getSubscription(org.id);
}

@Post('/cancel')
Expand All @@ -142,15 +176,15 @@ export class BillingController {
user.email
);

return this._stripeService.setToCancel(org.id);
return (await this.provider(org)).setToCancel(org.id);
}

@Post('/prorate')
prorate(
async prorate(
@GetOrgFromRequest() org: Organization,
@Body() body: BillingSubscribeDto
) {
return this._stripeService.prorate(org.id, body);
return (await this.provider(org)).prorate(org.id, body);
}

@Get('/charges')
Expand All @@ -162,7 +196,7 @@ export class BillingController {
throw new HttpException('Unauthorized', 400);
}

return this._stripeService.getCharges(org.id);
return (await this.provider(org)).getCharges(org.id);
}

@Post('/refund-charges')
Expand All @@ -175,7 +209,7 @@ export class BillingController {
throw new HttpException('Unauthorized', 400);
}

return this._stripeService.refundCharges(org.id, body.chargeIds);
return (await this.provider(org)).refundCharges(org.id, body.chargeIds);
}

@Post('/cancel-subscription')
Expand All @@ -187,7 +221,7 @@ export class BillingController {
throw new HttpException('Unauthorized', 400);
}

return this._stripeService.cancelSubscription(org.id);
return (await this.provider(org)).cancelSubscription(org.id);
}

@Get('/coupon-info')
Expand All @@ -199,7 +233,7 @@ export class BillingController {
throw new HttpException('Unauthorized', 400);
}

return this._stripeService.getCouponInfo(org.id);
return (await this.provider(org)).getCouponInfo(org.id);
}

@Post('/apply-coupon')
Expand All @@ -212,7 +246,7 @@ export class BillingController {
throw new HttpException('Unauthorized', 400);
}

return this._stripeService.applyCoupon(org.id, body);
return (await this.provider(org)).applyCoupon(org.id, body);
}

@Post('/cancel-coupon')
Expand All @@ -224,20 +258,20 @@ export class BillingController {
throw new HttpException('Unauthorized', 400);
}

return this._stripeService.cancelCoupon(org.id);
return (await this.provider(org)).cancelCoupon(org.id);
}

@Get('/chatbase-refund/preview')
chatbaseRefundPreview(@GetOrgFromRequest() org: Organization) {
return this._stripeService.chatbaseRefundPreview(org.id);
async chatbaseRefundPreview(@GetOrgFromRequest() org: Organization) {
return (await this.provider(org)).chatbaseRefundPreview(org.id);
}

@Post('/chatbase-refund')
async chatbaseRefund(
@GetUserFromRequest() user: User,
@GetOrgFromRequest() org: Organization
) {
const refund = await this._stripeService.chatbaseRefund(org.id);
const refund = await (await this.provider(org)).chatbaseRefund(org.id);

if (refund.refunded) {
await this._notificationService.sendEmail(
Expand All @@ -264,8 +298,8 @@ export class BillingController {
await this._subscriptionService.addSubscription(
org.id,
user.id,
body.subscription
body.subscription,
this._paymentService.getDefaultProviderName('web')
);
}

}
36 changes: 36 additions & 0 deletions apps/backend/src/api/routes/payment.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
Controller,
HttpException,
Param,
Post,
RawBodyRequest,
Req,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { PaymentService } from '@gitroom/nestjs-libraries/services/payment/payment.service';

@ApiTags('Payment')
@Controller('/payment')
export class PaymentController {
constructor(private readonly _paymentService: PaymentService) {}

@Post('/:provider')
async webhook(
@Param('provider') provider: string,
@Req() req: RawBodyRequest<Request>
) {
try {
return await this._paymentService.webhook(
provider,
req.rawBody,
// @ts-ignore
req.headers
);
} catch (e) {
if (e instanceof HttpException) {
throw e;
}
throw new HttpException(e, 500);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Passing a raw Error or unknown object e directly as the first argument to HttpException can lead to poor serialization (such as an empty object {} or a blank message) because Error properties like message are non-enumerable. It is better to extract the error message or pass a user-friendly generic message.

Suggested change
throw new HttpException(e, 500);
throw new HttpException((e as Error)?.message || 'Internal server error', 500);

}
}
}
Loading
Loading