Skip to content
Merged
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
23 changes: 23 additions & 0 deletions app/controllers/v2_ai_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class V2AiController < ApplicationController
before_action :authenticate_admin!

def generate_flashcards
topic = params[:topic]
count = params[:count] || 5
difficulty = params[:difficulty] || "intermedio"

suggestions = GenerativeAiService.generate_flashcards(topic, count, difficulty)

flashcards = suggestions.map do |s|
Flashcard.create!(
front: s["front"],
back: s["back"],
status: "waiting_approval"
)
end

render json: flashcards
rescue => e
render json: { error: e.message }, status: :unprocessable_entity
end
end
27 changes: 27 additions & 0 deletions app/controllers/v2_coupons_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class V2CouponsController < ApplicationController
before_action :authenticate_user!

def me
active_coupons = Current.user.coupons.merge(UserCoupon.active)
used_coupons = Current.user.coupons.merge(UserCoupon.used)

render json: {
active: active_coupons.map { |c| format_coupon(c, false) },
used: used_coupons.map { |c| format_coupon(c, true) }
}
end

private

def format_coupon(coupon, used)
{
id: coupon.id,
code: coupon.code,
description: coupon.description,
expiration_date: coupon.expiration_date,
coupon_type: coupon.coupon_type,
used: used,
expired: coupon.expired?
}
end
end
30 changes: 27 additions & 3 deletions app/controllers/v2_flashcards_controller.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
class V2FlashcardsController < ApplicationController
before_action :authenticate_user!

def create
flashcard = Flashcard.new(flashcard_params)
flashcard.user = Current.user
flashcard.status = "published" # Assuming user created ones are published immediately

if flashcard.save
# Automatically add to user's cards
Current.user.user_flashcards.create!(flashcard: flashcard)
render json: flashcard, status: :created
else
render json: { errors: flashcard.errors.full_messages }, status: :unprocessable_entity
end
end

def review
# Obtener flashcards pendientes de repaso para el usuario actual
user_flashcards = Current.user.user_flashcards.due.limit(20)

# Si no hay pendientes, podríamos sugerir algunas nuevas o simplemente enviar vacío
# El requisito dice que el backend decide según SRS

render json: {
flashcards: user_flashcards.map { |uf|
{
Expand All @@ -30,4 +41,17 @@ def answer
render json: { error: "Flashcard no encontrada para este usuario" }, status: :not_found
end
end

private

def flashcard_params
# Mapear specialtyId a category_id
p = params.permit(:front, :back, :specialtyId, :tags)
{
front: p[:front],
back: p[:back],
category_id: p[:specialtyId],
tags: p[:tags]
}
end
end
12 changes: 12 additions & 0 deletions app/models/coupon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Coupon < ApplicationRecord
has_many :user_coupons, dependent: :destroy
has_many :users, through: :user_coupons

validates :code, presence: true, uniqueness: true
validates :coupon_type, presence: true
validates :expiration_date, presence: true

def expired?
expiration_date < Time.current
end
end
8 changes: 8 additions & 0 deletions app/models/flashcard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@ class Flashcard < ApplicationRecord
has_many :users, through: :user_flashcards

validates :front, :back, presence: true

# status: pending, published, deleted, waiting_approval, not_approved
STATUSES = %w[pending published deleted waiting_approval not_approved].freeze
validates :status, inclusion: { in: STATUSES }, allow_nil: true

def tag_list
tags&.split(",")&.map(&:strip) || []
end
end
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class User < ApplicationRecord

has_many :user_flashcards, dependent: :destroy
has_many :flashcards, through: :user_flashcards
has_many :user_coupons, dependent: :destroy
has_many :coupons, through: :user_coupons

# Validaciones
validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: VALID_EMAIL_REGEX }
Expand Down
9 changes: 9 additions & 0 deletions app/models/user_coupon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class UserCoupon < ApplicationRecord
belongs_to :user
belongs_to :coupon

validates :user_id, uniqueness: { scope: :coupon_id, message: "ya ha usado este cupón" }

scope :used, -> { where.not(used_at: nil) }
scope :active, -> { where(used_at: nil) }
end
13 changes: 13 additions & 0 deletions app/services/generative_ai_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ def self.generate_clinical_case(prompt)
JSON.parse(response.content)
end

def self.generate_flashcards(topic, count, difficulty)
prompt = "Genera #{count} flashcards de medicina sobre el tema '#{topic}' con dificultad #{difficulty}. " \
"Cada flashcard debe tener un 'front' (pregunta o concepto) y un 'back' (respuesta corta o explicación). " \
"Devuelve un array JSON de objetos con las llaves 'front' y 'back'."

response = client.generate_content(
{ contents: { role: "user", parts: { text: prompt } } }
)

json_content = response.content.gsub(/```json\n?/, "").gsub(/```\n?/, "").strip
JSON.parse(json_content)
end

def self.parse_pdf_to_exam(file)
file_content = Base64.strict_encode64(file.read)

Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
Rails.application.routes.draw do
# V2 Endpoints
get "v2/leaderboard/national", to: "v2_leaderboard#national"
get "v2/coupons/me", to: "v2_coupons#me"
get "v2/images/bank", to: "v2_images#bank"
get "v2/flashcards/review", to: "v2_flashcards#review"
post "v2/flashcards/review/:id/answer", to: "v2_flashcards#answer"
post "v2/flashcards", to: "v2_flashcards#create"
get "v2/knowledge-base", to: "v2_knowledge_base#index"
get "v2/errors/summary", to: "v2_errors#summary"

Expand Down Expand Up @@ -55,6 +57,7 @@
post "ai/generate_question", to: "ai#generate_question"
post "ai/generate_clinical_case", to: "ai#generate_clinical_case"
post "ai/bulk_create_exam", to: "ai#bulk_create_exam"
post "v2/ai/generate-flashcards", to: "v2_ai#generate_flashcards"

get "leaderboard", to: "leaderboard#index"

Expand Down
12 changes: 12 additions & 0 deletions db/migrate/20260313222720_create_coupons.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateCoupons < ActiveRecord::Migration[8.1]
def change
create_table :coupons do |t|
t.string :code
t.datetime :expiration_date
t.text :description
t.string :coupon_type

t.timestamps
end
end
end
11 changes: 11 additions & 0 deletions db/migrate/20260313222722_create_user_coupons.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateUserCoupons < ActiveRecord::Migration[8.1]
def change
create_table :user_coupons do |t|
t.references :user, null: false, foreign_key: true
t.references :coupon, null: false, foreign_key: true
t.datetime :used_at

t.timestamps
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddStatusAndTagsToFlashcards < ActiveRecord::Migration[8.1]
def change
add_column :flashcards, :status, :string
add_column :flashcards, :tags, :string
end
end
25 changes: 24 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions test/controllers/v2_ai_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require "test_helper"

class V2AiControllerTest < ActionDispatch::IntegrationTest
fixtures :users
setup do
@admin = users(:admin)
@user = users(:player_one)
end

test "should generate flashcards as admin" do
GenerativeAiService.stubs(:generate_flashcards).returns([
{ "front" => "AI Front 1", "back" => "AI Back 1" },
{ "front" => "AI Front 2", "back" => "AI Back 2" }
])

assert_difference("Flashcard.count", 2) do
post "/v2/ai/generate-flashcards",
params: { topic: "Diabetes", count: 2, difficulty: "fácil" },
headers: admin_auth_headers(@admin)
end
assert_response :success

data = json_response
assert_equal 2, data.size
assert_equal "waiting_approval", data.first["status"]
end

test "should fail to generate flashcards as regular user" do
post "/v2/ai/generate-flashcards",
params: { topic: "Diabetes" },
headers: player_auth_headers(@user)
assert_response :forbidden
end
end
28 changes: 28 additions & 0 deletions test/controllers/v2_coupons_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require "test_helper"

class V2CouponsControllerTest < ActionDispatch::IntegrationTest
fixtures :users
setup do
@user = users(:player_one)
@coupon1 = Coupon.create!(code: "DESC10", coupon_type: "percentage", expiration_date: 1.day.from_now, description: "10% off")
@coupon2 = Coupon.create!(code: "FREE", coupon_type: "fixed", expiration_date: 1.day.from_now, description: "Free")
@user.user_coupons.create!(coupon: @coupon1)
@user.user_coupons.create!(coupon: @coupon2, used_at: Time.current)
end

test "should get me coupons" do
get "/v2/coupons/me", headers: player_auth_headers(@user)
assert_response :success

data = json_response
assert_equal 1, data["active"].size
assert_equal 1, data["used"].size
assert_equal "DESC10", data["active"].first["code"]
assert_equal "FREE", data["used"].first["code"]
end

test "should fail if not authenticated" do
get "/v2/coupons/me"
assert_response :unauthorized
end
end
33 changes: 33 additions & 0 deletions test/controllers/v2_flashcards_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require "test_helper"

class V2FlashcardsControllerTest < ActionDispatch::IntegrationTest
fixtures :users, :categories
setup do
@user = users(:player_one)
@category = categories(:one)
end

test "should create flashcard" do
assert_difference("Flashcard.count", 1) do
assert_difference("UserFlashcard.count", 1) do
post "/v2/flashcards",
params: { front: "Front text", back: "Back text", specialtyId: @category.id, tags: "tag1, tag2" },
headers: player_auth_headers(@user)
end
end
assert_response :created

flashcard = Flashcard.last
assert_equal "Front text", flashcard.front
assert_equal "published", flashcard.status
assert_equal "tag1, tag2", flashcard.tags
assert_equal @category.id, flashcard.category_id
end

test "should fail to create flashcard with invalid params" do
post "/v2/flashcards",
params: { front: "" },
headers: player_auth_headers(@user)
assert_response :unprocessable_entity
end
end