Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Dependencies
/vendor
/node_modules

# Environment
.env
.env.*
!.env.example

# IDE
/.idea
/.vscode
/.fleet
/.zed
/.nova

# OS
.DS_Store
Thumbs.db

# Storage
/storage/logs/*
/storage/framework/cache/*
/storage/framework/sessions/*
/storage/framework/views/*
!/storage/framework/.gitignore

# Build artifacts
/public/hot

# Stack de deploy (contém credenciais)
/docker/getfy-stack.yml

# Misc
*.log
.phpunit.result.cache
/.phpunit.cache
48 changes: 48 additions & 0 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build & Push Docker Image

on:
push:
branches:
- main
workflow_dispatch:

env:
IMAGE_NAME: impa365/getfyimpa

jobs:
build-and-push:
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,4 @@ Homestead.yaml
Thumbs.db
/docs
/Docs Gateways
docs
Docs Gateways
/docker/getfy-stack.yml
27 changes: 24 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,46 @@ FROM php:8.2-cli-alpine AS php_base

RUN apk add --no-cache \
git unzip libzip-dev libpng-dev oniguruma-dev \
mysql-client icu-dev libxml2-dev $PHPIZE_DEPS
mysql-client postgresql-client postgresql-dev icu-dev libxml2-dev \
freetype-dev libjpeg-turbo-dev $PHPIZE_DEPS

RUN docker-php-ext-configure gd --with-freetype --with-jpeg

RUN pecl install redis \
&& docker-php-ext-enable redis

RUN docker-php-ext-install pdo_mysql zip exif intl opcache pcntl bcmath
RUN docker-php-ext-install pdo_mysql pdo_pgsql zip exif intl opcache pcntl bcmath gd

RUN git config --global --add safe.directory /var/www/html

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

# ---------- Build frontend assets ----------
FROM node:20-alpine AS frontend

WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY vite.config.js ./
COPY resources/ ./resources/
COPY public/ ./public/
RUN npm run build

# ---------- Final app image ----------
FROM php_base AS app

COPY . .
COPY --from=frontend /app/public/build public/build
COPY docker/entrypoint.sh /usr/local/bin/getfy-entrypoint

RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --no-scripts

RUN chmod +x /usr/local/bin/getfy-entrypoint \
&& mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views bootstrap/cache .docker \
&& chmod -R 777 storage bootstrap/cache .docker
&& chmod -R 777 storage bootstrap/cache .docker \
&& git config --global --add safe.directory /var/www/html

EXPOSE 80

Expand Down
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,3 @@ Se o seu painel não suportar `docker-compose.yml`, a alternativa mais simples
- Arquivos em `public/storage` não aparecem: `storage:link` pode falhar em hospedagens sem symlink. Se acontecer, crie manualmente um link/symlink de `public/storage` → `storage/app/public` (ou use um painel que suporte isso).
- Rotinas automáticas não rodam: configure o cron pela URL `/cron?token=...` a cada minuto.

Se você deseja apoiar o desenvolvimento diretamente:
| Pix | Chave |
|---|---|
| Aleatória | `ce05f7d1-27db-4d46-bca5-0a80c621349a` |

12 changes: 10 additions & 2 deletions app/Http/Controllers/CheckoutController.php
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ public function process(Request $request): RedirectResponse|JsonResponse
'status' => 'pending',
'gateway' => null,
'gateway_id' => null,
'payment_method' => 'pix',
]));
$order->load('orderItems');
event(new OrderPending($order));
Expand Down Expand Up @@ -716,6 +717,7 @@ public function process(Request $request): RedirectResponse|JsonResponse
'status' => 'pending',
'gateway' => null,
'gateway_id' => null,
'payment_method' => 'pix',
]));
$order->load('orderItems');
event(new OrderPending($order));
Expand Down Expand Up @@ -822,6 +824,7 @@ public function process(Request $request): RedirectResponse|JsonResponse
'status' => 'pending',
'gateway' => null,
'gateway_id' => null,
'payment_method' => 'pix',
]));
$order->load('orderItems');
event(new OrderPending($order));
Expand Down Expand Up @@ -960,6 +963,7 @@ public function process(Request $request): RedirectResponse|JsonResponse
'status' => 'pending',
'gateway' => null,
'gateway_id' => null,
'payment_method' => 'card',
]));
$order->load('orderItems');
event(new OrderPending($order));
Expand Down Expand Up @@ -1043,7 +1047,8 @@ public function process(Request $request): RedirectResponse|JsonResponse
if (! empty($customRedirect) && is_string($customRedirect)) {
$redirectUrl = $customRedirect;
} else {
$next = ($order->user_id && User::find($order->user_id)) ? 'member-area' : 'login';
$hasMemberArea = $order->product && in_array($order->product->type, [Product::TYPE_AREA_MEMBROS, Product::TYPE_APLICATIVO], true);
$next = ($hasMemberArea && $order->user_id && User::find($order->user_id)) ? 'member-area' : 'login';
$redirectUrl = route('checkout.thank-you', ['order_id' => $order->id, 'next' => $next]);
}
}
Expand Down Expand Up @@ -1090,6 +1095,7 @@ public function process(Request $request): RedirectResponse|JsonResponse
'status' => 'pending',
'gateway' => null,
'gateway_id' => null,
'payment_method' => 'boleto',
]));
$order->load('orderItems');
event(new OrderPending($order));
Expand Down Expand Up @@ -1168,6 +1174,7 @@ public function process(Request $request): RedirectResponse|JsonResponse
$order = $createOrderAndItems(array_merge($orderPayload, [
'status' => 'completed',
'gateway' => 'manual',
'payment_method' => 'manual',
]));
$updateCheckoutSession($order);
$order->load('orderItems');
Expand Down Expand Up @@ -1400,7 +1407,8 @@ public function orderStatus(Request $request): JsonResponse
if (! empty($customRedirect) && is_string($customRedirect)) {
$redirectUrl = $customRedirect;
} else {
$next = ($order->user_id && User::find($order->user_id)) ? 'member-area' : 'login';
$hasMemberArea = $order->product && in_array($order->product->type, [Product::TYPE_AREA_MEMBROS, Product::TYPE_APLICATIVO], true);
$next = ($hasMemberArea && $order->user_id && User::find($order->user_id)) ? 'member-area' : 'login';
$redirectUrl = route('checkout.thank-you', ['order_id' => $order->id, 'next' => $next]);
}
}
Expand Down
4 changes: 2 additions & 2 deletions app/Http/Controllers/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private function buildGraficoVendas(?int $tenantId, string $period, ?string $sta

if ($isHourly) {
$rows = $query
->selectRaw('HOUR(created_at) as hora, SUM(amount) as total')
->selectRaw('EXTRACT(HOUR FROM created_at) as hora, SUM(amount) as total')
->groupBy('hora')
->orderBy('hora')
->get()
Expand All @@ -179,7 +179,7 @@ private function buildGraficoVendas(?int $tenantId, string $period, ?string $sta
}

$rows = $query
->selectRaw('DATE(created_at) as data, SUM(amount) as total')
->selectRaw('CAST(created_at AS DATE) as data, SUM(amount) as total')
->groupBy('data')
->orderBy('data')
->get();
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/EmailTestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function sendTest(Request $request)
Mail::purge('smtp');

try {
$appName = config('getfy.app_name');
$appName = Setting::get('app_name', config('getfy.app_name'), $tenantId);
$body = "<p>Este é um e-mail de teste enviado por {$appName}.</p>";
Mail::mailer('smtp')->to($validated['test_to'])->send(new \App\Mail\TestEmail('E‑mail de teste - '.$appName, $body));
return response()->json(['success' => true]);
Expand Down
80 changes: 60 additions & 20 deletions app/Http/Controllers/MemberAreaAppController.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,19 +89,25 @@ public function modulos(Request $request, string $slug): Response
'id' => $s->id,
'title' => $s->title,
'cover_mode' => $s->cover_mode ?? 'vertical',
'modules' => $s->modules->map(fn ($m) => [
'id' => $m->id,
'title' => $m->title,
'thumbnail' => $m->thumbnail,
'show_title_on_cover' => $m->show_title_on_cover ?? true,
'lessons' => $m->lessons->map(fn (MemberLesson $l) => [
'id' => $l->id,
'title' => $l->title,
'type' => $l->type,
'duration_seconds' => $l->duration_seconds,
'is_completed' => $this->isLessonCompleted($user->id, $l->id),
])->values()->all(),
])->values()->all(),
'modules' => $s->modules->map(function ($m) use ($user) {
$unlocksAt = $m->dripUnlocksAt($user->id);
$isLocked = $unlocksAt !== null;
return [
'id' => $m->id,
'title' => $m->title,
'thumbnail' => $m->thumbnail,
'show_title_on_cover' => $m->show_title_on_cover ?? true,
'is_locked' => $isLocked,
'unlocks_at' => $unlocksAt?->toIso8601String(),
'lessons' => $isLocked ? [] : $m->lessons->map(fn (MemberLesson $l) => [
'id' => $l->id,
'title' => $l->title,
'type' => $l->type,
'duration_seconds' => $l->duration_seconds,
'is_completed' => $this->isLessonCompleted($user->id, $l->id),
])->values()->all(),
];
})->values()->all(),
])->values()->all(),
'base_url' => $this->baseUrlForRequest($product, $request),
'slug' => $slug,
Expand All @@ -116,6 +122,16 @@ public function moduleContent(Request $request, string $slug, MemberModule $modu
abort(404);
}
$user = $request->user();

// Drip content: bloqueia acesso se módulo ainda não liberado
$unlocksAt = $module->dripUnlocksAt($user->id);
if ($unlocksAt !== null) {
if ($request->header('X-Inertia')) {
return redirect()->back()->with('error', 'Este módulo será liberado em '.$unlocksAt->format('d/m/Y').'.');
}
abort(403, 'Conteúdo ainda não liberado.');
}

$module->load(['section', 'lessons' => fn ($q) => $q->orderBy('position')]);
$lessons = $module->lessons->map(fn (MemberLesson $l) => [
'id' => $l->id,
Expand Down Expand Up @@ -160,12 +176,17 @@ public function moduleContent(Request $request, string $slug, MemberModule $modu
'id' => $s->id,
'title' => $s->title,
'cover_mode' => $s->cover_mode ?? 'vertical',
'modules' => $s->modules->map(fn ($m) => [
'id' => $m->id,
'title' => $m->title,
'thumbnail' => $m->thumbnail,
'show_title_on_cover' => $m->show_title_on_cover ?? true,
])->values()->all(),
'modules' => $s->modules->map(function ($m) use ($user) {
$unlocksAt = $m->dripUnlocksAt($user->id);
return [
'id' => $m->id,
'title' => $m->title,
'thumbnail' => $m->thumbnail,
'show_title_on_cover' => $m->show_title_on_cover ?? true,
'is_locked' => $unlocksAt !== null,
'unlocks_at' => $unlocksAt?->toIso8601String(),
];
})->values()->all(),
])->values()->all();

$config = $product->member_area_config;
Expand Down Expand Up @@ -221,6 +242,19 @@ public function lesson(Request $request, string $slug, MemberLesson $lesson): Re
abort(404);
}
$user = $request->user();

// Drip content: bloqueia acesso se módulo da aula ainda não liberado
$lesson->loadMissing('module');
if ($lesson->module) {
$unlocksAt = $lesson->module->dripUnlocksAt($user->id);
if ($unlocksAt !== null) {
if ($request->header('X-Inertia')) {
return redirect()->back()->with('error', 'Este conteúdo será liberado em '.$unlocksAt->format('d/m/Y').'.');
}
abort(403, 'Conteúdo ainda não liberado.');
}
}

$this->progressService->ensureLessonStarted($lesson, $user);
$lesson->load('module.section');

Expand Down Expand Up @@ -793,12 +827,18 @@ private function mapModuleForMemberArea(MemberModule $m, MemberSection $s, Produ
$sectionType = $s->section_type ?? 'courses';

if ($sectionType === 'courses') {
$unlocksAt = $m->dripUnlocksAt($user->id);
$isLocked = $unlocksAt !== null;

return [
'id' => $m->id,
'title' => $m->title,
'thumbnail' => $m->thumbnail,
'show_title_on_cover' => $m->show_title_on_cover ?? true,
'lessons' => $m->lessons->map(fn (MemberLesson $l) => [
'is_locked' => $isLocked,
'unlocks_at' => $unlocksAt?->toIso8601String(),
'release_after_days' => $m->release_after_days,
'lessons' => $isLocked ? [] : $m->lessons->map(fn (MemberLesson $l) => [
'id' => $l->id,
'title' => $l->title,
'type' => $l->type,
Expand Down
Loading