mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-12 07:31:02 +00:00
feat(p2p): add Phase 4 merchant tier system and migrations
- Add merchant tier system (Lite/Super/Diamond) with tier badges - Add advanced order filters (token, fiat, payment method, amount range) - Add merchant dashboard with stats, ads management, tier upgrade - Add fraud prevention system with risk scoring and trade limits - Rename migrations to timestamp format for Supabase CLI compatibility - Add new migrations: phase2_phase3_tables, fraud_prevention, merchant_system
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
|
||||
# dotenvx
|
||||
.env.keys
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -0,0 +1,382 @@
|
||||
# For detailed configuration reference documentation, visit:
|
||||
# https://supabase.com/docs/guides/local-development/cli/config
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "web"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. `public` and `graphql_public` schemas are included by default.
|
||||
schemas = ["public", "graphql_public"]
|
||||
# Extra schemas to add to the search_path of every request.
|
||||
extra_search_path = ["public", "extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[api.tls]
|
||||
# Enable HTTPS endpoints locally using a self-signed certificate.
|
||||
enabled = false
|
||||
# Paths to self-signed certificate pair.
|
||||
# cert_path = "../certs/my-cert.pem"
|
||||
# key_path = "../certs/my-key.pem"
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 17
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
# How many server connections to allow per user/database pair.
|
||||
default_pool_size = 20
|
||||
# Maximum number of client connections allowed.
|
||||
max_client_conn = 100
|
||||
|
||||
# [db.vault]
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[db.migrations]
|
||||
# If disabled, migrations will be skipped during a db push or reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of schema files that describe your database.
|
||||
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
|
||||
schema_paths = []
|
||||
|
||||
[db.seed]
|
||||
# If enabled, seeds the database after migrations during a db reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of seed files to load during db reset.
|
||||
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
||||
sql_paths = ["./seed.sql"]
|
||||
|
||||
[db.network_restrictions]
|
||||
# Enable management of network restrictions.
|
||||
enabled = false
|
||||
# List of IPv4 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs = ["0.0.0.0/0"]
|
||||
# List of IPv6 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs_v6 = ["::/0"]
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||
# ip_version = "IPv6"
|
||||
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||
# max_header_length = 4096
|
||||
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
# External URL of the API server that frontend connects to.
|
||||
api_url = "http://127.0.0.1"
|
||||
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||
openai_api_key = "env(OPENAI_API_KEY)"
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
[storage]
|
||||
enabled = true
|
||||
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||
file_size_limit = "50MiB"
|
||||
|
||||
# Uncomment to configure local storage buckets
|
||||
# [storage.buckets.images]
|
||||
# public = false
|
||||
# file_size_limit = "50MiB"
|
||||
# allowed_mime_types = ["image/png", "image/jpeg"]
|
||||
# objects_path = "./images"
|
||||
|
||||
# Allow connections via S3 compatible clients
|
||||
[storage.s3_protocol]
|
||||
enabled = true
|
||||
|
||||
# Image transformation API is available to Supabase Pro plan.
|
||||
# [storage.image_transformation]
|
||||
# enabled = true
|
||||
|
||||
# Store analytical data in S3 for running ETL jobs over Iceberg Catalog
|
||||
# This feature is only available on the hosted platform.
|
||||
[storage.analytics]
|
||||
enabled = false
|
||||
max_namespaces = 5
|
||||
max_tables = 10
|
||||
max_catalogs = 2
|
||||
|
||||
# Analytics Buckets is available to Supabase Pro plan.
|
||||
# [storage.analytics.buckets.my-warehouse]
|
||||
|
||||
# Store vector embeddings in S3 for large and durable datasets
|
||||
# This feature is only available on the hosted platform.
|
||||
[storage.vector]
|
||||
enabled = false
|
||||
max_buckets = 10
|
||||
max_indexes = 5
|
||||
|
||||
# Vector Buckets is available to Supabase Pro plan.
|
||||
# [storage.vector.buckets.documents-openai]
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://127.0.0.1:3000"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:<port>/auth/v1).
|
||||
# jwt_issuer = ""
|
||||
# Path to JWT signing key. DO NOT commit your signing keys file to git.
|
||||
# signing_keys_path = "./signing_keys.json"
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
# Requires enable_refresh_token_rotation = true.
|
||||
refresh_token_reuse_interval = 10
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
# Allow/disallow anonymous sign-ins to your project.
|
||||
enable_anonymous_sign_ins = false
|
||||
# Allow/disallow testing manual linking of accounts
|
||||
enable_manual_linking = false
|
||||
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
|
||||
minimum_password_length = 6
|
||||
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
|
||||
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
|
||||
password_requirements = ""
|
||||
|
||||
[auth.rate_limit]
|
||||
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
|
||||
email_sent = 2
|
||||
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
|
||||
sms_sent = 30
|
||||
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
|
||||
anonymous_users = 30
|
||||
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
|
||||
token_refresh = 150
|
||||
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
|
||||
sign_in_sign_ups = 30
|
||||
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
|
||||
token_verifications = 30
|
||||
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
|
||||
web3 = 30
|
||||
|
||||
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
|
||||
# [auth.captcha]
|
||||
# enabled = true
|
||||
# provider = "hcaptcha"
|
||||
# secret = ""
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = false
|
||||
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
|
||||
secure_password_change = false
|
||||
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||
max_frequency = "1s"
|
||||
# Number of characters used in the email OTP.
|
||||
otp_length = 6
|
||||
# Number of seconds before the email OTP expires (defaults to 1 hour).
|
||||
otp_expiry = 3600
|
||||
|
||||
# Use a production-ready SMTP server
|
||||
# [auth.email.smtp]
|
||||
# enabled = true
|
||||
# host = "smtp.sendgrid.net"
|
||||
# port = 587
|
||||
# user = "apikey"
|
||||
# pass = "env(SENDGRID_API_KEY)"
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
# Uncomment to customize email template
|
||||
# [auth.email.template.invite]
|
||||
# subject = "You have been invited"
|
||||
# content_path = "./supabase/templates/invite.html"
|
||||
|
||||
# Uncomment to customize notification email template
|
||||
# [auth.email.notification.password_changed]
|
||||
# enabled = true
|
||||
# subject = "Your password has been changed"
|
||||
# content_path = "./templates/password_changed_notification.html"
|
||||
|
||||
[auth.sms]
|
||||
# Allow/disallow new user signups via SMS to your project.
|
||||
enable_signup = false
|
||||
# If enabled, users need to confirm their phone number before signing in.
|
||||
enable_confirmations = false
|
||||
# Template for sending OTP to users
|
||||
template = "Your code is {{ .Code }}"
|
||||
# Controls the minimum amount of time that must pass before sending another sms otp.
|
||||
max_frequency = "5s"
|
||||
|
||||
# Use pre-defined map of phone number to OTP for testing.
|
||||
# [auth.sms.test_otp]
|
||||
# 4152127777 = "123456"
|
||||
|
||||
# Configure logged in session timeouts.
|
||||
# [auth.sessions]
|
||||
# Force log out after the specified duration.
|
||||
# timebox = "24h"
|
||||
# Force log out if the user has been inactive longer than the specified duration.
|
||||
# inactivity_timeout = "8h"
|
||||
|
||||
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
|
||||
# [auth.hook.before_user_created]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://postgres/auth/before-user-created-hook"
|
||||
|
||||
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||
# [auth.hook.custom_access_token]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://<database>/<schema>/<hook_name>"
|
||||
|
||||
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
||||
[auth.sms.twilio]
|
||||
enabled = false
|
||||
account_sid = ""
|
||||
message_service_sid = ""
|
||||
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||
|
||||
# Multi-factor-authentication is available to Supabase Pro plan.
|
||||
[auth.mfa]
|
||||
# Control how many MFA factors can be enrolled at once per user.
|
||||
max_enrolled_factors = 10
|
||||
|
||||
# Control MFA via App Authenticator (TOTP)
|
||||
[auth.mfa.totp]
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
|
||||
# Configure MFA via Phone Messaging
|
||||
[auth.mfa.phone]
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
otp_length = 6
|
||||
template = "Your code is {{ .Code }}"
|
||||
max_frequency = "5s"
|
||||
|
||||
# Configure MFA via WebAuthn
|
||||
# [auth.mfa.web_authn]
|
||||
# enroll_enabled = true
|
||||
# verify_enabled = true
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
[auth.external.apple]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
||||
# Overrides the default auth redirectUrl.
|
||||
redirect_uri = ""
|
||||
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||
# or any other third-party OIDC providers.
|
||||
url = ""
|
||||
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||
skip_nonce_check = false
|
||||
# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address.
|
||||
email_optional = false
|
||||
|
||||
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
|
||||
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
|
||||
[auth.web3.solana]
|
||||
enabled = false
|
||||
|
||||
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.firebase]
|
||||
enabled = false
|
||||
# project_id = "my-firebase-project"
|
||||
|
||||
# Use Auth0 as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.auth0]
|
||||
enabled = false
|
||||
# tenant = "my-auth0-tenant"
|
||||
# tenant_region = "us"
|
||||
|
||||
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.aws_cognito]
|
||||
enabled = false
|
||||
# user_pool_id = "my-user-pool-id"
|
||||
# user_pool_region = "us-east-1"
|
||||
|
||||
# Use Clerk as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.clerk]
|
||||
enabled = false
|
||||
# Obtain from https://clerk.com/setup/supabase
|
||||
# domain = "example.clerk.accounts.dev"
|
||||
|
||||
# OAuth server configuration
|
||||
[auth.oauth_server]
|
||||
# Enable OAuth server functionality
|
||||
enabled = false
|
||||
# Path for OAuth consent flow UI
|
||||
authorization_url_path = "/oauth/consent"
|
||||
# Allow dynamic client registration
|
||||
allow_dynamic_registration = false
|
||||
|
||||
[edge_runtime]
|
||||
enabled = true
|
||||
# Supported request policies: `oneshot`, `per_worker`.
|
||||
# `per_worker` (default) — enables hot reload during local development.
|
||||
# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks).
|
||||
policy = "per_worker"
|
||||
# Port to attach the Chrome inspector for debugging edge functions.
|
||||
inspector_port = 8083
|
||||
# The Deno major version to use.
|
||||
deno_version = 2
|
||||
|
||||
# [edge_runtime.secrets]
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[analytics]
|
||||
enabled = true
|
||||
port = 54327
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
|
||||
# Experimental features may be deprecated any time
|
||||
[experimental]
|
||||
# Configures Postgres storage engine to use OrioleDB (S3)
|
||||
orioledb_version = ""
|
||||
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
||||
s3_host = "env(S3_HOST)"
|
||||
# Configures S3 bucket region, eg. us-east-1
|
||||
s3_region = "env(S3_REGION)"
|
||||
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
||||
s3_access_key = "env(S3_ACCESS_KEY)"
|
||||
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
||||
s3_secret_key = "env(S3_SECRET_KEY)"
|
||||
+23
-19
@@ -30,8 +30,8 @@ CREATE TABLE IF NOT EXISTS public.payment_methods (
|
||||
CONSTRAINT unique_payment_method UNIQUE (currency, method_name)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_payment_methods_currency_active ON public.payment_methods(currency, is_active);
|
||||
CREATE INDEX idx_payment_methods_type ON public.payment_methods(method_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_methods_currency_active ON public.payment_methods(currency, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_methods_type ON public.payment_methods(method_type);
|
||||
|
||||
-- =====================================================
|
||||
-- P2P FIAT OFFERS TABLE
|
||||
@@ -84,10 +84,10 @@ CREATE TABLE IF NOT EXISTS public.p2p_fiat_offers (
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_p2p_offers_seller ON public.p2p_fiat_offers(seller_id);
|
||||
CREATE INDEX idx_p2p_offers_currency ON public.p2p_fiat_offers(fiat_currency, token);
|
||||
CREATE INDEX idx_p2p_offers_status ON public.p2p_fiat_offers(status)WHERE status IN ('open', 'paused');
|
||||
CREATE INDEX idx_p2p_offers_active ON public.p2p_fiat_offers(status, fiat_currency, token)
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_offers_seller ON public.p2p_fiat_offers(seller_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_offers_currency ON public.p2p_fiat_offers(fiat_currency, token);
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_offers_status ON public.p2p_fiat_offers(status)WHERE status IN ('open', 'paused');
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_offers_active ON public.p2p_fiat_offers(status, fiat_currency, token)
|
||||
WHERE status = 'open' AND remaining_amount > 0;
|
||||
|
||||
-- =====================================================
|
||||
@@ -141,11 +141,11 @@ CREATE TABLE IF NOT EXISTS public.p2p_fiat_trades (
|
||||
CONSTRAINT different_users CHECK (seller_id != buyer_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_p2p_trades_offer ON public.p2p_fiat_trades(offer_id);
|
||||
CREATE INDEX idx_p2p_trades_seller ON public.p2p_fiat_trades(seller_id);
|
||||
CREATE INDEX idx_p2p_trades_buyer ON public.p2p_fiat_trades(buyer_id);
|
||||
CREATE INDEX idx_p2p_trades_status ON public.p2p_fiat_trades(status);
|
||||
CREATE INDEX idx_p2p_trades_deadlines ON public.p2p_fiat_trades(payment_deadline, confirmation_deadline)
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_trades_offer ON public.p2p_fiat_trades(offer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_trades_seller ON public.p2p_fiat_trades(seller_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_trades_buyer ON public.p2p_fiat_trades(buyer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_trades_status ON public.p2p_fiat_trades(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_trades_deadlines ON public.p2p_fiat_trades(payment_deadline, confirmation_deadline)
|
||||
WHERE status IN ('pending', 'payment_sent');
|
||||
|
||||
-- =====================================================
|
||||
@@ -185,9 +185,9 @@ CREATE TABLE IF NOT EXISTS public.p2p_fiat_disputes (
|
||||
CONSTRAINT one_dispute_per_trade UNIQUE (trade_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_disputes_trade ON public.p2p_fiat_disputes(trade_id);
|
||||
CREATE INDEX idx_disputes_status ON public.p2p_fiat_disputes(status)WHERE status IN ('open', 'under_review');
|
||||
CREATE INDEX idx_disputes_moderator ON public.p2p_fiat_disputes(assigned_moderator_id) WHERE status = 'under_review';
|
||||
CREATE INDEX IF NOT EXISTS idx_disputes_trade ON public.p2p_fiat_disputes(trade_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_disputes_status ON public.p2p_fiat_disputes(status)WHERE status IN ('open', 'under_review');
|
||||
CREATE INDEX IF NOT EXISTS idx_disputes_moderator ON public.p2p_fiat_disputes(assigned_moderator_id) WHERE status = 'under_review';
|
||||
|
||||
-- =====================================================
|
||||
-- P2P REPUTATION TABLE
|
||||
@@ -233,8 +233,8 @@ CREATE TABLE IF NOT EXISTS public.p2p_reputation (
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_reputation_score ON public.p2p_reputation(reputation_score DESC);
|
||||
CREATE INDEX idx_reputation_verified ON public.p2p_reputation(verified_merchant) WHERE verified_merchant = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_reputation_score ON public.p2p_reputation(reputation_score DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_reputation_verified ON public.p2p_reputation(verified_merchant) WHERE verified_merchant = true;
|
||||
|
||||
-- =====================================================
|
||||
-- PLATFORM ESCROW TRACKING
|
||||
@@ -264,9 +264,9 @@ CREATE TABLE IF NOT EXISTS public.p2p_audit_log (
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_log_user ON public.p2p_audit_log(user_id, created_at DESC);
|
||||
CREATE INDEX idx_audit_log_entity ON public.p2p_audit_log(entity_type, entity_id);
|
||||
CREATE INDEX idx_audit_log_created ON public.p2p_audit_log(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_user ON public.p2p_audit_log(user_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON public.p2p_audit_log(entity_type, entity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON public.p2p_audit_log(created_at DESC);
|
||||
|
||||
-- =====================================================
|
||||
-- TRIGGERS FOR UPDATED_AT
|
||||
@@ -279,15 +279,19 @@ BEGIN
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS update_payment_methods_updated_at ON public.payment_methods;
|
||||
CREATE TRIGGER update_payment_methods_updated_at BEFORE UPDATE ON public.payment_methods
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
DROP TRIGGER IF EXISTS update_p2p_offers_updated_at ON public.p2p_fiat_offers;
|
||||
CREATE TRIGGER update_p2p_offers_updated_at BEFORE UPDATE ON public.p2p_fiat_offers
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
DROP TRIGGER IF EXISTS update_p2p_trades_updated_at ON public.p2p_fiat_trades;
|
||||
CREATE TRIGGER update_p2p_trades_updated_at BEFORE UPDATE ON public.p2p_fiat_trades
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
DROP TRIGGER IF EXISTS update_p2p_disputes_updated_at ON public.p2p_fiat_disputes;
|
||||
CREATE TRIGGER update_p2p_disputes_updated_at BEFORE UPDATE ON public.p2p_fiat_disputes
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
-- =====================================================
|
||||
-- P2P FIAT SYSTEM - PHASE 2 & 3 ADDITIONAL TABLES
|
||||
-- Messages, Ratings, Notifications, Evidence, Fraud Reports
|
||||
-- =====================================================
|
||||
|
||||
-- =====================================================
|
||||
-- P2P MESSAGES TABLE (Phase 2 - Chat System)
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_messages (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trade_id UUID NOT NULL REFERENCES public.p2p_fiat_trades(id) ON DELETE CASCADE,
|
||||
sender_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
|
||||
-- Message content
|
||||
message TEXT NOT NULL CHECK (LENGTH(message) > 0 AND LENGTH(message) <= 2000),
|
||||
message_type VARCHAR(20) DEFAULT 'text' CHECK (message_type IN ('text', 'image', 'system')),
|
||||
attachment_url TEXT, -- Supabase Storage URL
|
||||
|
||||
-- Read status
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
read_at TIMESTAMPTZ,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}',
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes for p2p_messages
|
||||
CREATE INDEX idx_p2p_messages_trade ON public.p2p_messages(trade_id, created_at DESC);
|
||||
CREATE INDEX idx_p2p_messages_sender ON public.p2p_messages(sender_id);
|
||||
CREATE INDEX idx_p2p_messages_unread ON public.p2p_messages(trade_id, is_read) WHERE is_read = false;
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_messages ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy: Only trade participants can read/write messages
|
||||
CREATE POLICY "p2p_messages_trade_participants" ON public.p2p_messages
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.p2p_fiat_trades t
|
||||
WHERE t.id = trade_id
|
||||
AND (t.seller_id = auth.uid() OR t.buyer_id = auth.uid())
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- P2P RATINGS TABLE (Phase 2 - Trust System)
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_ratings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
trade_id UUID NOT NULL REFERENCES public.p2p_fiat_trades(id) ON DELETE CASCADE,
|
||||
rater_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
rated_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
|
||||
-- Rating details
|
||||
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
||||
review TEXT CHECK (LENGTH(review) <= 500),
|
||||
|
||||
-- Rating aspects (optional breakdown)
|
||||
communication_rating INT CHECK (communication_rating BETWEEN 1 AND 5),
|
||||
speed_rating INT CHECK (speed_rating BETWEEN 1 AND 5),
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT unique_rating_per_trade UNIQUE(trade_id, rater_id),
|
||||
CONSTRAINT cannot_rate_self CHECK (rater_id != rated_id)
|
||||
);
|
||||
|
||||
-- Indexes for p2p_ratings
|
||||
CREATE INDEX idx_p2p_ratings_trade ON public.p2p_ratings(trade_id);
|
||||
CREATE INDEX idx_p2p_ratings_rated ON public.p2p_ratings(rated_id, created_at DESC);
|
||||
CREATE INDEX idx_p2p_ratings_rater ON public.p2p_ratings(rater_id);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_ratings ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy: Public read (for reputation display)
|
||||
CREATE POLICY "p2p_ratings_public_read" ON public.p2p_ratings
|
||||
FOR SELECT USING (true);
|
||||
|
||||
-- Policy: Only trade participants can insert ratings
|
||||
CREATE POLICY "p2p_ratings_trade_participants_insert" ON public.p2p_ratings
|
||||
FOR INSERT WITH CHECK (
|
||||
rater_id = auth.uid() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.p2p_fiat_trades t
|
||||
WHERE t.id = trade_id
|
||||
AND t.status = 'completed'
|
||||
AND (t.seller_id = auth.uid() OR t.buyer_id = auth.uid())
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- P2P NOTIFICATIONS TABLE (Phase 2 - Real-time Alerts)
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Notification content
|
||||
type VARCHAR(50) NOT NULL CHECK (type IN (
|
||||
'new_order', 'payment_sent', 'payment_confirmed', 'trade_cancelled',
|
||||
'dispute_opened', 'dispute_resolved', 'new_message', 'rating_received',
|
||||
'offer_matched', 'trade_reminder', 'system'
|
||||
)),
|
||||
title TEXT NOT NULL,
|
||||
message TEXT,
|
||||
|
||||
-- Reference
|
||||
reference_type VARCHAR(20) CHECK (reference_type IN ('trade', 'offer', 'dispute', 'message')),
|
||||
reference_id UUID,
|
||||
|
||||
-- Status
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
read_at TIMESTAMPTZ,
|
||||
|
||||
-- Action URL (frontend route)
|
||||
action_url TEXT,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}',
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes for p2p_notifications
|
||||
CREATE INDEX idx_p2p_notifications_user ON public.p2p_notifications(user_id, created_at DESC);
|
||||
CREATE INDEX idx_p2p_notifications_unread ON public.p2p_notifications(user_id, is_read) WHERE is_read = false;
|
||||
CREATE INDEX idx_p2p_notifications_type ON public.p2p_notifications(user_id, type);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_notifications ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy: Users can only see their own notifications
|
||||
CREATE POLICY "p2p_notifications_own" ON public.p2p_notifications
|
||||
FOR ALL USING (user_id = auth.uid());
|
||||
|
||||
-- =====================================================
|
||||
-- P2P DISPUTE EVIDENCE TABLE (Phase 3 - Dispute System)
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_dispute_evidence (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
dispute_id UUID NOT NULL REFERENCES public.p2p_fiat_disputes(id) ON DELETE CASCADE,
|
||||
uploaded_by UUID NOT NULL REFERENCES auth.users(id),
|
||||
|
||||
-- Evidence details
|
||||
evidence_type VARCHAR(30) NOT NULL CHECK (evidence_type IN (
|
||||
'screenshot', 'receipt', 'bank_statement', 'chat_log',
|
||||
'transaction_proof', 'identity_doc', 'other'
|
||||
)),
|
||||
file_url TEXT NOT NULL,
|
||||
file_name TEXT,
|
||||
file_size INT, -- bytes
|
||||
mime_type TEXT,
|
||||
|
||||
-- Description
|
||||
description TEXT CHECK (LENGTH(description) <= 1000),
|
||||
|
||||
-- Admin review
|
||||
reviewed_by UUID REFERENCES auth.users(id),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
review_notes TEXT,
|
||||
is_valid BOOLEAN,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes for p2p_dispute_evidence
|
||||
CREATE INDEX idx_p2p_evidence_dispute ON public.p2p_dispute_evidence(dispute_id, created_at);
|
||||
CREATE INDEX idx_p2p_evidence_uploader ON public.p2p_dispute_evidence(uploaded_by);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_dispute_evidence ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy: Dispute parties can view evidence
|
||||
CREATE POLICY "p2p_evidence_parties_read" ON public.p2p_dispute_evidence
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.p2p_fiat_disputes d
|
||||
JOIN public.p2p_fiat_trades t ON t.id = d.trade_id
|
||||
WHERE d.id = dispute_id
|
||||
AND (t.seller_id = auth.uid() OR t.buyer_id = auth.uid() OR d.opened_by = auth.uid())
|
||||
)
|
||||
);
|
||||
|
||||
-- Policy: Dispute parties can upload evidence
|
||||
CREATE POLICY "p2p_evidence_parties_insert" ON public.p2p_dispute_evidence
|
||||
FOR INSERT WITH CHECK (
|
||||
uploaded_by = auth.uid() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.p2p_fiat_disputes d
|
||||
JOIN public.p2p_fiat_trades t ON t.id = d.trade_id
|
||||
WHERE d.id = dispute_id
|
||||
AND d.status IN ('open', 'under_review')
|
||||
AND (t.seller_id = auth.uid() OR t.buyer_id = auth.uid())
|
||||
)
|
||||
);
|
||||
|
||||
-- Policy: Admins can view all evidence
|
||||
CREATE POLICY "p2p_evidence_admin_read" ON public.p2p_dispute_evidence
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.admin_roles
|
||||
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- Policy: Admins can update evidence (for review)
|
||||
CREATE POLICY "p2p_evidence_admin_update" ON public.p2p_dispute_evidence
|
||||
FOR UPDATE USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.admin_roles
|
||||
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- P2P FRAUD REPORTS TABLE (Phase 3 - Security)
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_fraud_reports (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
reporter_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
reported_user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
|
||||
-- Report details
|
||||
trade_id UUID REFERENCES public.p2p_fiat_trades(id),
|
||||
reason VARCHAR(50) NOT NULL CHECK (reason IN (
|
||||
'fake_payment', 'fake_proof', 'scam_attempt', 'harassment',
|
||||
'money_laundering', 'identity_fraud', 'multiple_accounts', 'other'
|
||||
)),
|
||||
description TEXT NOT NULL CHECK (LENGTH(description) >= 20 AND LENGTH(description) <= 2000),
|
||||
evidence_urls TEXT[] DEFAULT '{}',
|
||||
|
||||
-- Investigation
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN (
|
||||
'pending', 'investigating', 'confirmed', 'dismissed', 'escalated'
|
||||
)),
|
||||
assigned_to UUID REFERENCES auth.users(id),
|
||||
assigned_at TIMESTAMPTZ,
|
||||
|
||||
-- Resolution
|
||||
resolution TEXT,
|
||||
resolution_notes TEXT,
|
||||
resolved_by UUID REFERENCES auth.users(id),
|
||||
resolved_at TIMESTAMPTZ,
|
||||
|
||||
-- Action taken
|
||||
action_taken VARCHAR(30) CHECK (action_taken IN (
|
||||
'warning_issued', 'temporary_ban', 'permanent_ban',
|
||||
'trade_restricted', 'no_action', 'referred_to_authorities'
|
||||
)),
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT cannot_report_self CHECK (reporter_id != reported_user_id)
|
||||
);
|
||||
|
||||
-- Indexes for p2p_fraud_reports
|
||||
CREATE INDEX idx_p2p_fraud_reporter ON public.p2p_fraud_reports(reporter_id);
|
||||
CREATE INDEX idx_p2p_fraud_reported ON public.p2p_fraud_reports(reported_user_id);
|
||||
CREATE INDEX idx_p2p_fraud_status ON public.p2p_fraud_reports(status) WHERE status IN ('pending', 'investigating');
|
||||
CREATE INDEX idx_p2p_fraud_trade ON public.p2p_fraud_reports(trade_id) WHERE trade_id IS NOT NULL;
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_fraud_reports ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policy: Users can view their own reports
|
||||
CREATE POLICY "p2p_fraud_own_reports" ON public.p2p_fraud_reports
|
||||
FOR SELECT USING (reporter_id = auth.uid());
|
||||
|
||||
-- Policy: Users can create reports
|
||||
CREATE POLICY "p2p_fraud_create" ON public.p2p_fraud_reports
|
||||
FOR INSERT WITH CHECK (reporter_id = auth.uid());
|
||||
|
||||
-- Policy: Admins can view all reports
|
||||
CREATE POLICY "p2p_fraud_admin_read" ON public.p2p_fraud_reports
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.admin_roles
|
||||
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- Policy: Admins can update reports
|
||||
CREATE POLICY "p2p_fraud_admin_update" ON public.p2p_fraud_reports
|
||||
FOR UPDATE USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.admin_roles
|
||||
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- TRIGGERS FOR UPDATED_AT
|
||||
-- =====================================================
|
||||
CREATE TRIGGER update_p2p_messages_updated_at BEFORE UPDATE ON public.p2p_messages
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_p2p_fraud_reports_updated_at BEFORE UPDATE ON public.p2p_fraud_reports
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- =====================================================
|
||||
-- ENABLE REALTIME FOR NEW TABLES
|
||||
-- =====================================================
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE public.p2p_messages;
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE public.p2p_notifications;
|
||||
|
||||
-- =====================================================
|
||||
-- NOTIFICATION TRIGGER FUNCTIONS
|
||||
-- =====================================================
|
||||
|
||||
-- Function to create notification
|
||||
CREATE OR REPLACE FUNCTION public.create_p2p_notification(
|
||||
p_user_id UUID,
|
||||
p_type TEXT,
|
||||
p_title TEXT,
|
||||
p_message TEXT DEFAULT NULL,
|
||||
p_reference_type TEXT DEFAULT NULL,
|
||||
p_reference_id UUID DEFAULT NULL,
|
||||
p_action_url TEXT DEFAULT NULL,
|
||||
p_metadata JSONB DEFAULT '{}'
|
||||
) RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_notification_id UUID;
|
||||
BEGIN
|
||||
INSERT INTO public.p2p_notifications (
|
||||
user_id, type, title, message,
|
||||
reference_type, reference_id, action_url, metadata
|
||||
) VALUES (
|
||||
p_user_id, p_type, p_title, p_message,
|
||||
p_reference_type, p_reference_id, p_action_url, p_metadata
|
||||
)
|
||||
RETURNING id INTO v_notification_id;
|
||||
|
||||
RETURN v_notification_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Trigger: Notify on new trade
|
||||
CREATE OR REPLACE FUNCTION notify_on_new_trade()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_offer RECORD;
|
||||
BEGIN
|
||||
-- Get offer details
|
||||
SELECT * INTO v_offer FROM public.p2p_fiat_offers WHERE id = NEW.offer_id;
|
||||
|
||||
-- Notify seller about new order
|
||||
PERFORM public.create_p2p_notification(
|
||||
NEW.seller_id,
|
||||
'new_order',
|
||||
'New P2P Order',
|
||||
format('Someone wants to buy %s %s', NEW.crypto_amount, v_offer.token),
|
||||
'trade',
|
||||
NEW.id,
|
||||
format('/p2p/trade/%s', NEW.id)
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trigger_notify_new_trade
|
||||
AFTER INSERT ON public.p2p_fiat_trades
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_on_new_trade();
|
||||
|
||||
-- Trigger: Notify on payment sent
|
||||
CREATE OR REPLACE FUNCTION notify_on_payment_sent()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF OLD.status = 'pending' AND NEW.status = 'payment_sent' THEN
|
||||
PERFORM public.create_p2p_notification(
|
||||
NEW.seller_id,
|
||||
'payment_sent',
|
||||
'Payment Marked as Sent',
|
||||
'The buyer has marked the payment as sent. Please verify and release.',
|
||||
'trade',
|
||||
NEW.id,
|
||||
format('/p2p/trade/%s', NEW.id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trigger_notify_payment_sent
|
||||
AFTER UPDATE ON public.p2p_fiat_trades
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_on_payment_sent();
|
||||
|
||||
-- Trigger: Notify on trade completed
|
||||
CREATE OR REPLACE FUNCTION notify_on_trade_completed()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF OLD.status != 'completed' AND NEW.status = 'completed' THEN
|
||||
-- Notify buyer
|
||||
PERFORM public.create_p2p_notification(
|
||||
NEW.buyer_id,
|
||||
'payment_confirmed',
|
||||
'Trade Completed!',
|
||||
'The seller has released the crypto. Check your wallet.',
|
||||
'trade',
|
||||
NEW.id,
|
||||
format('/p2p/trade/%s', NEW.id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trigger_notify_trade_completed
|
||||
AFTER UPDATE ON public.p2p_fiat_trades
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_on_trade_completed();
|
||||
|
||||
-- Trigger: Notify on dispute opened
|
||||
CREATE OR REPLACE FUNCTION notify_on_dispute_opened()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_trade RECORD;
|
||||
v_other_party UUID;
|
||||
BEGIN
|
||||
-- Get trade details
|
||||
SELECT * INTO v_trade FROM public.p2p_fiat_trades WHERE id = NEW.trade_id;
|
||||
|
||||
-- Determine other party
|
||||
IF NEW.opened_by = v_trade.seller_id THEN
|
||||
v_other_party := v_trade.buyer_id;
|
||||
ELSE
|
||||
v_other_party := v_trade.seller_id;
|
||||
END IF;
|
||||
|
||||
-- Notify the other party
|
||||
PERFORM public.create_p2p_notification(
|
||||
v_other_party,
|
||||
'dispute_opened',
|
||||
'Dispute Opened',
|
||||
'A dispute has been opened for your trade. Please provide evidence.',
|
||||
'dispute',
|
||||
NEW.id,
|
||||
format('/p2p/dispute/%s', NEW.id)
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trigger_notify_dispute_opened
|
||||
AFTER INSERT ON public.p2p_fiat_disputes
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_on_dispute_opened();
|
||||
|
||||
-- Trigger: Notify on new message
|
||||
CREATE OR REPLACE FUNCTION notify_on_new_message()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
v_trade RECORD;
|
||||
v_recipient_id UUID;
|
||||
BEGIN
|
||||
-- Skip system messages
|
||||
IF NEW.message_type = 'system' THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Get trade details
|
||||
SELECT * INTO v_trade FROM public.p2p_fiat_trades WHERE id = NEW.trade_id;
|
||||
|
||||
-- Determine recipient
|
||||
IF NEW.sender_id = v_trade.seller_id THEN
|
||||
v_recipient_id := v_trade.buyer_id;
|
||||
ELSE
|
||||
v_recipient_id := v_trade.seller_id;
|
||||
END IF;
|
||||
|
||||
-- Create notification
|
||||
PERFORM public.create_p2p_notification(
|
||||
v_recipient_id,
|
||||
'new_message',
|
||||
'New Message',
|
||||
LEFT(NEW.message, 100),
|
||||
'trade',
|
||||
NEW.trade_id,
|
||||
format('/p2p/trade/%s', NEW.trade_id)
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trigger_notify_new_message
|
||||
AFTER INSERT ON public.p2p_messages
|
||||
FOR EACH ROW EXECUTE FUNCTION notify_on_new_message();
|
||||
|
||||
-- =====================================================
|
||||
-- RATING HELPER FUNCTION
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.update_user_rating_stats(p_user_id UUID)
|
||||
RETURNS void AS $$
|
||||
DECLARE
|
||||
v_avg_rating NUMERIC;
|
||||
v_total_ratings INT;
|
||||
BEGIN
|
||||
-- Calculate average rating
|
||||
SELECT
|
||||
AVG(rating)::NUMERIC(3,2),
|
||||
COUNT(*)
|
||||
INTO v_avg_rating, v_total_ratings
|
||||
FROM public.p2p_ratings
|
||||
WHERE rated_id = p_user_id;
|
||||
|
||||
-- Update reputation with rating info
|
||||
UPDATE public.p2p_reputation
|
||||
SET
|
||||
updated_at = NOW()
|
||||
-- Note: You could add avg_rating column to p2p_reputation if needed
|
||||
WHERE user_id = p_user_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Trigger: Update stats after rating
|
||||
CREATE OR REPLACE FUNCTION update_rating_stats_trigger()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM public.update_user_rating_stats(NEW.rated_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trigger_update_rating_stats
|
||||
AFTER INSERT ON public.p2p_ratings
|
||||
FOR EACH ROW EXECUTE FUNCTION update_rating_stats_trigger();
|
||||
|
||||
-- =====================================================
|
||||
-- GRANT EXECUTE PERMISSIONS
|
||||
-- =====================================================
|
||||
GRANT EXECUTE ON FUNCTION public.create_p2p_notification TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.update_user_rating_stats TO authenticated;
|
||||
|
||||
-- =====================================================
|
||||
-- ADD INDEXES FOR PERFORMANCE
|
||||
-- =====================================================
|
||||
|
||||
-- Index for finding user's average rating quickly
|
||||
CREATE INDEX idx_p2p_ratings_avg ON public.p2p_ratings(rated_id, rating);
|
||||
|
||||
-- Index for unread notification count
|
||||
CREATE INDEX idx_p2p_notifications_unread_count ON public.p2p_notifications(user_id)
|
||||
WHERE is_read = false;
|
||||
|
||||
-- =====================================================
|
||||
-- COMMENTS FOR DOCUMENTATION
|
||||
-- =====================================================
|
||||
COMMENT ON TABLE public.p2p_messages IS 'Real-time chat messages between trade participants';
|
||||
COMMENT ON TABLE public.p2p_ratings IS 'Post-trade ratings and reviews';
|
||||
COMMENT ON TABLE public.p2p_notifications IS 'User notifications for P2P trading events';
|
||||
COMMENT ON TABLE public.p2p_dispute_evidence IS 'Evidence files uploaded during disputes';
|
||||
COMMENT ON TABLE public.p2p_fraud_reports IS 'Fraud reports submitted by users';
|
||||
@@ -0,0 +1,456 @@
|
||||
-- =====================================================
|
||||
-- P2P FRAUD PREVENTION SYSTEM
|
||||
-- Auto-detection rules and triggers
|
||||
-- =====================================================
|
||||
|
||||
-- =====================================================
|
||||
-- USER FRAUD INDICATORS TABLE
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_user_fraud_indicators (
|
||||
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Trade statistics
|
||||
cancel_rate DECIMAL(5,2) DEFAULT 0, -- Percentage
|
||||
dispute_rate DECIMAL(5,2) DEFAULT 0, -- Percentage
|
||||
avg_trade_amount DECIMAL(18,2) DEFAULT 0,
|
||||
|
||||
-- Recent activity
|
||||
recent_cancellations_24h INT DEFAULT 0,
|
||||
recent_disputes_7d INT DEFAULT 0,
|
||||
trades_today INT DEFAULT 0,
|
||||
volume_today DECIMAL(18,2) DEFAULT 0,
|
||||
|
||||
-- Risk assessment
|
||||
risk_score INT DEFAULT 0 CHECK (risk_score BETWEEN 0 AND 100),
|
||||
risk_level VARCHAR(10) DEFAULT 'low' CHECK (risk_level IN ('low', 'medium', 'high', 'critical')),
|
||||
active_flags TEXT[] DEFAULT '{}',
|
||||
|
||||
-- Restrictions
|
||||
is_blocked BOOLEAN DEFAULT FALSE,
|
||||
blocked_reason TEXT,
|
||||
blocked_at TIMESTAMPTZ,
|
||||
blocked_until TIMESTAMPTZ,
|
||||
requires_review BOOLEAN DEFAULT FALSE,
|
||||
|
||||
-- Cooldowns
|
||||
last_cancellation_at TIMESTAMPTZ,
|
||||
last_dispute_at TIMESTAMPTZ,
|
||||
last_trade_at TIMESTAMPTZ,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_fraud_indicators_risk ON public.p2p_user_fraud_indicators(risk_score DESC);
|
||||
CREATE INDEX idx_fraud_indicators_blocked ON public.p2p_user_fraud_indicators(is_blocked) WHERE is_blocked = true;
|
||||
CREATE INDEX idx_fraud_indicators_review ON public.p2p_user_fraud_indicators(requires_review) WHERE requires_review = true;
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_user_fraud_indicators ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Users can view their own indicators (limited)
|
||||
CREATE POLICY "p2p_fraud_indicators_own_read" ON public.p2p_user_fraud_indicators
|
||||
FOR SELECT USING (user_id = auth.uid());
|
||||
|
||||
-- Admins can view and update all
|
||||
CREATE POLICY "p2p_fraud_indicators_admin" ON public.p2p_user_fraud_indicators
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.admin_roles
|
||||
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- SUSPICIOUS ACTIVITY LOG
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_suspicious_activity (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
trade_id UUID REFERENCES public.p2p_fiat_trades(id),
|
||||
|
||||
-- Activity details
|
||||
activity_type VARCHAR(50) NOT NULL CHECK (activity_type IN (
|
||||
'high_cancel_rate', 'frequent_disputes', 'rapid_trading',
|
||||
'unusual_amount', 'new_account_large_trade', 'payment_name_mismatch',
|
||||
'suspected_multi_account', 'ip_anomaly', 'device_anomaly', 'other'
|
||||
)),
|
||||
severity VARCHAR(10) NOT NULL CHECK (severity IN ('low', 'medium', 'high', 'critical')),
|
||||
description TEXT,
|
||||
metadata JSONB DEFAULT '{}',
|
||||
|
||||
-- Resolution
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'dismissed', 'actioned')),
|
||||
reviewed_by UUID REFERENCES auth.users(id),
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
action_taken TEXT,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_suspicious_activity_user ON public.p2p_suspicious_activity(user_id, created_at DESC);
|
||||
CREATE INDEX idx_suspicious_activity_status ON public.p2p_suspicious_activity(status) WHERE status = 'pending';
|
||||
CREATE INDEX idx_suspicious_activity_severity ON public.p2p_suspicious_activity(severity, created_at DESC);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_suspicious_activity ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Only admins can view
|
||||
CREATE POLICY "p2p_suspicious_activity_admin" ON public.p2p_suspicious_activity
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.admin_roles
|
||||
WHERE user_id = auth.uid() AND role IN ('moderator', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- FUNCTION: Calculate User Risk Score
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.calculate_user_risk_score(p_user_id UUID)
|
||||
RETURNS TABLE(
|
||||
risk_score INT,
|
||||
risk_level TEXT,
|
||||
flags TEXT[]
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_indicators RECORD;
|
||||
v_score INT := 0;
|
||||
v_flags TEXT[] := '{}';
|
||||
BEGIN
|
||||
-- Get current indicators
|
||||
SELECT * INTO v_indicators
|
||||
FROM public.p2p_user_fraud_indicators
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
-- If no record exists, create one
|
||||
IF NOT FOUND THEN
|
||||
INSERT INTO public.p2p_user_fraud_indicators (user_id)
|
||||
VALUES (p_user_id)
|
||||
RETURNING * INTO v_indicators;
|
||||
END IF;
|
||||
|
||||
-- Calculate from reputation data
|
||||
SELECT
|
||||
COALESCE(
|
||||
CASE WHEN total_trades > 0
|
||||
THEN (cancelled_trades::DECIMAL / total_trades * 100)
|
||||
ELSE 0
|
||||
END, 0),
|
||||
COALESCE(
|
||||
CASE WHEN total_trades > 0
|
||||
THEN (disputed_trades::DECIMAL / total_trades * 100)
|
||||
ELSE 0
|
||||
END, 0)
|
||||
INTO v_indicators.cancel_rate, v_indicators.dispute_rate
|
||||
FROM public.p2p_reputation
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
-- Apply rules and calculate score
|
||||
-- Rule 1: High cancel rate (>30%)
|
||||
IF v_indicators.cancel_rate > 30 THEN
|
||||
v_score := v_score + 25;
|
||||
v_flags := array_append(v_flags, 'High Cancellation Rate');
|
||||
END IF;
|
||||
|
||||
-- Rule 2: High dispute rate (>20%)
|
||||
IF v_indicators.dispute_rate > 20 THEN
|
||||
v_score := v_score + 30;
|
||||
v_flags := array_append(v_flags, 'Frequent Disputes');
|
||||
END IF;
|
||||
|
||||
-- Rule 3: Multiple recent cancellations (>3 in 24h)
|
||||
IF v_indicators.recent_cancellations_24h > 3 THEN
|
||||
v_score := v_score + 35;
|
||||
v_flags := array_append(v_flags, 'Multiple Recent Cancellations');
|
||||
END IF;
|
||||
|
||||
-- Rule 4: Recent disputes (>2 in 7d)
|
||||
IF v_indicators.recent_disputes_7d > 2 THEN
|
||||
v_score := v_score + 25;
|
||||
v_flags := array_append(v_flags, 'Recent Disputes');
|
||||
END IF;
|
||||
|
||||
-- Cap score at 100
|
||||
v_score := LEAST(v_score, 100);
|
||||
|
||||
-- Determine risk level
|
||||
risk_level := CASE
|
||||
WHEN v_score >= 95 THEN 'critical'
|
||||
WHEN v_score >= 80 THEN 'high'
|
||||
WHEN v_score >= 50 THEN 'medium'
|
||||
ELSE 'low'
|
||||
END;
|
||||
|
||||
-- Update indicators table
|
||||
UPDATE public.p2p_user_fraud_indicators
|
||||
SET
|
||||
risk_score = v_score,
|
||||
risk_level = risk_level,
|
||||
active_flags = v_flags,
|
||||
is_blocked = (v_score >= 95),
|
||||
requires_review = (v_score >= 80),
|
||||
updated_at = NOW()
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
risk_score := v_score;
|
||||
flags := v_flags;
|
||||
|
||||
RETURN NEXT;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- FUNCTION: Check Trade Allowed
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.check_trade_allowed(
|
||||
p_user_id UUID,
|
||||
p_trade_amount DECIMAL
|
||||
) RETURNS TABLE(
|
||||
allowed BOOLEAN,
|
||||
reason TEXT
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_indicators RECORD;
|
||||
v_reputation RECORD;
|
||||
v_limits RECORD;
|
||||
v_cooldown_ms BIGINT;
|
||||
BEGIN
|
||||
-- Get fraud indicators
|
||||
SELECT * INTO v_indicators
|
||||
FROM public.p2p_user_fraud_indicators
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
-- Check if blocked
|
||||
IF v_indicators IS NOT NULL AND v_indicators.is_blocked THEN
|
||||
allowed := FALSE;
|
||||
reason := COALESCE(v_indicators.blocked_reason, 'Your account is temporarily restricted from trading.');
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Check cooldown after cancellation (5 minutes)
|
||||
IF v_indicators IS NOT NULL AND v_indicators.last_cancellation_at IS NOT NULL THEN
|
||||
v_cooldown_ms := EXTRACT(EPOCH FROM (NOW() - v_indicators.last_cancellation_at)) * 1000;
|
||||
IF v_cooldown_ms < 300000 THEN -- 5 minutes
|
||||
allowed := FALSE;
|
||||
reason := format('Please wait %s seconds before creating a new trade.', ((300000 - v_cooldown_ms) / 1000)::INT);
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Check cooldown after dispute (24 hours)
|
||||
IF v_indicators IS NOT NULL AND v_indicators.last_dispute_at IS NOT NULL THEN
|
||||
v_cooldown_ms := EXTRACT(EPOCH FROM (NOW() - v_indicators.last_dispute_at)) * 1000;
|
||||
IF v_cooldown_ms < 86400000 THEN -- 24 hours
|
||||
allowed := FALSE;
|
||||
reason := 'Trading is restricted for 24 hours after a dispute.';
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Get reputation for trust level
|
||||
SELECT * INTO v_reputation
|
||||
FROM public.p2p_reputation
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
-- Define limits based on trust level
|
||||
SELECT * INTO v_limits FROM (
|
||||
SELECT
|
||||
CASE COALESCE(v_reputation.trust_level, 'new')
|
||||
WHEN 'verified' THEN 50000
|
||||
WHEN 'advanced' THEN 10000
|
||||
WHEN 'intermediate' THEN 2000
|
||||
WHEN 'basic' THEN 500
|
||||
ELSE 100
|
||||
END as max_trade,
|
||||
CASE COALESCE(v_reputation.trust_level, 'new')
|
||||
WHEN 'verified' THEN 50
|
||||
WHEN 'advanced' THEN 20
|
||||
WHEN 'intermediate' THEN 10
|
||||
WHEN 'basic' THEN 5
|
||||
ELSE 3
|
||||
END as max_daily_trades,
|
||||
CASE COALESCE(v_reputation.trust_level, 'new')
|
||||
WHEN 'verified' THEN 100000
|
||||
WHEN 'advanced' THEN 25000
|
||||
WHEN 'intermediate' THEN 5000
|
||||
WHEN 'basic' THEN 1000
|
||||
ELSE 200
|
||||
END as max_daily_volume
|
||||
) limits;
|
||||
|
||||
-- Check trade amount limit
|
||||
IF p_trade_amount > v_limits.max_trade THEN
|
||||
allowed := FALSE;
|
||||
reason := format('Trade amount exceeds your limit of $%s. Complete more trades to increase your limit.', v_limits.max_trade);
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Check daily trade count
|
||||
IF v_indicators IS NOT NULL AND v_indicators.trades_today >= v_limits.max_daily_trades THEN
|
||||
allowed := FALSE;
|
||||
reason := format('You have reached your daily limit of %s trades.', v_limits.max_daily_trades);
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Check daily volume
|
||||
IF v_indicators IS NOT NULL AND (v_indicators.volume_today + p_trade_amount) > v_limits.max_daily_volume THEN
|
||||
allowed := FALSE;
|
||||
reason := format('This trade would exceed your daily volume limit of $%s.', v_limits.max_daily_volume);
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- All checks passed
|
||||
allowed := TRUE;
|
||||
reason := NULL;
|
||||
RETURN NEXT;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- FUNCTION: Log Suspicious Activity
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.log_suspicious_activity(
|
||||
p_user_id UUID,
|
||||
p_trade_id UUID,
|
||||
p_activity_type TEXT,
|
||||
p_severity TEXT,
|
||||
p_description TEXT DEFAULT NULL,
|
||||
p_metadata JSONB DEFAULT '{}'
|
||||
) RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_activity_id UUID;
|
||||
BEGIN
|
||||
INSERT INTO public.p2p_suspicious_activity (
|
||||
user_id, trade_id, activity_type, severity, description, metadata
|
||||
) VALUES (
|
||||
p_user_id, p_trade_id, p_activity_type, p_severity, p_description, p_metadata
|
||||
)
|
||||
RETURNING id INTO v_activity_id;
|
||||
|
||||
-- Auto-recalculate risk score
|
||||
PERFORM public.calculate_user_risk_score(p_user_id);
|
||||
|
||||
RETURN v_activity_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- TRIGGER: Update Fraud Indicators on Trade Completion
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION update_fraud_indicators_on_trade()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- On trade completion
|
||||
IF NEW.status = 'completed' AND (OLD.status IS NULL OR OLD.status != 'completed') THEN
|
||||
-- Update buyer indicators
|
||||
INSERT INTO public.p2p_user_fraud_indicators (user_id, trades_today, volume_today, last_trade_at)
|
||||
VALUES (NEW.buyer_id, 1, NEW.fiat_amount, NOW())
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
trades_today = p2p_user_fraud_indicators.trades_today + 1,
|
||||
volume_today = p2p_user_fraud_indicators.volume_today + NEW.fiat_amount,
|
||||
last_trade_at = NOW(),
|
||||
updated_at = NOW();
|
||||
|
||||
-- Update seller indicators
|
||||
INSERT INTO public.p2p_user_fraud_indicators (user_id, trades_today, volume_today, last_trade_at)
|
||||
VALUES (NEW.seller_id, 1, NEW.fiat_amount, NOW())
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
trades_today = p2p_user_fraud_indicators.trades_today + 1,
|
||||
volume_today = p2p_user_fraud_indicators.volume_today + NEW.fiat_amount,
|
||||
last_trade_at = NOW(),
|
||||
updated_at = NOW();
|
||||
END IF;
|
||||
|
||||
-- On trade cancellation
|
||||
IF NEW.status = 'cancelled' AND (OLD.status IS NULL OR OLD.status != 'cancelled') THEN
|
||||
IF NEW.cancelled_by IS NOT NULL THEN
|
||||
UPDATE public.p2p_user_fraud_indicators
|
||||
SET
|
||||
recent_cancellations_24h = recent_cancellations_24h + 1,
|
||||
last_cancellation_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE user_id = NEW.cancelled_by;
|
||||
|
||||
-- Recalculate risk score
|
||||
PERFORM public.calculate_user_risk_score(NEW.cancelled_by);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- On dispute
|
||||
IF NEW.status = 'disputed' AND (OLD.status IS NULL OR OLD.status != 'disputed') THEN
|
||||
-- Update both parties
|
||||
UPDATE public.p2p_user_fraud_indicators
|
||||
SET
|
||||
recent_disputes_7d = recent_disputes_7d + 1,
|
||||
last_dispute_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE user_id IN (NEW.buyer_id, NEW.seller_id);
|
||||
|
||||
-- Recalculate risk scores
|
||||
PERFORM public.calculate_user_risk_score(NEW.buyer_id);
|
||||
PERFORM public.calculate_user_risk_score(NEW.seller_id);
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trigger_update_fraud_indicators
|
||||
AFTER UPDATE ON public.p2p_fiat_trades
|
||||
FOR EACH ROW EXECUTE FUNCTION update_fraud_indicators_on_trade();
|
||||
|
||||
-- =====================================================
|
||||
-- SCHEDULED JOB: Reset Daily Counters
|
||||
-- (Run at midnight UTC via pg_cron or external scheduler)
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.reset_daily_fraud_counters()
|
||||
RETURNS void AS $$
|
||||
BEGIN
|
||||
UPDATE public.p2p_user_fraud_indicators
|
||||
SET
|
||||
trades_today = 0,
|
||||
volume_today = 0,
|
||||
updated_at = NOW();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- SCHEDULED JOB: Reset Weekly Counters
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.reset_weekly_fraud_counters()
|
||||
RETURNS void AS $$
|
||||
BEGIN
|
||||
UPDATE public.p2p_user_fraud_indicators
|
||||
SET
|
||||
recent_disputes_7d = 0,
|
||||
updated_at = NOW();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- GRANT PERMISSIONS
|
||||
-- =====================================================
|
||||
GRANT EXECUTE ON FUNCTION public.calculate_user_risk_score TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.check_trade_allowed TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.log_suspicious_activity TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.reset_daily_fraud_counters TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.reset_weekly_fraud_counters TO authenticated;
|
||||
|
||||
-- =====================================================
|
||||
-- COMMENTS
|
||||
-- =====================================================
|
||||
COMMENT ON TABLE public.p2p_user_fraud_indicators IS 'Tracks fraud indicators and risk scores for P2P users';
|
||||
COMMENT ON TABLE public.p2p_suspicious_activity IS 'Log of suspicious activities detected by the fraud system';
|
||||
COMMENT ON FUNCTION public.calculate_user_risk_score IS 'Calculate and update user risk score based on trading behavior';
|
||||
COMMENT ON FUNCTION public.check_trade_allowed IS 'Check if a user is allowed to make a trade based on limits and restrictions';
|
||||
@@ -0,0 +1,534 @@
|
||||
-- =====================================================
|
||||
-- P2P MERCHANT SYSTEM - PHASE 4
|
||||
-- Merchant tiers, stats, and advanced features
|
||||
-- =====================================================
|
||||
|
||||
-- =====================================================
|
||||
-- MERCHANT TIERS TABLE
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_merchant_tiers (
|
||||
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tier info
|
||||
tier VARCHAR(20) DEFAULT 'lite' CHECK (tier IN ('lite', 'super', 'diamond')),
|
||||
|
||||
-- Deposit (for higher tiers)
|
||||
deposit_amount DECIMAL(18,2) DEFAULT 0,
|
||||
deposit_token VARCHAR(10) DEFAULT 'HEZ',
|
||||
deposit_tx_hash TEXT,
|
||||
deposit_locked_at TIMESTAMPTZ,
|
||||
|
||||
-- Limits based on tier
|
||||
max_pending_orders INT DEFAULT 5,
|
||||
max_order_amount DECIMAL(18,2) DEFAULT 10000,
|
||||
featured_ads_allowed INT DEFAULT 0,
|
||||
|
||||
-- Application status
|
||||
application_status VARCHAR(20) CHECK (application_status IN ('pending', 'approved', 'rejected', 'suspended')),
|
||||
applied_at TIMESTAMPTZ,
|
||||
applied_for_tier VARCHAR(20),
|
||||
approved_at TIMESTAMPTZ,
|
||||
approved_by UUID REFERENCES auth.users(id),
|
||||
rejection_reason TEXT,
|
||||
|
||||
-- Review
|
||||
last_review_at TIMESTAMPTZ,
|
||||
next_review_at TIMESTAMPTZ,
|
||||
review_notes TEXT,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_merchant_tiers_tier ON public.p2p_merchant_tiers(tier);
|
||||
CREATE INDEX idx_merchant_tiers_pending ON public.p2p_merchant_tiers(application_status)
|
||||
WHERE application_status = 'pending';
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_merchant_tiers ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Users can view their own tier
|
||||
CREATE POLICY "p2p_merchant_own_read" ON public.p2p_merchant_tiers
|
||||
FOR SELECT USING (user_id = auth.uid());
|
||||
|
||||
-- Users can apply for tier (insert/update own)
|
||||
CREATE POLICY "p2p_merchant_own_apply" ON public.p2p_merchant_tiers
|
||||
FOR INSERT WITH CHECK (user_id = auth.uid());
|
||||
|
||||
CREATE POLICY "p2p_merchant_own_update" ON public.p2p_merchant_tiers
|
||||
FOR UPDATE USING (user_id = auth.uid())
|
||||
WITH CHECK (
|
||||
-- Can only update application_status to 'pending' and applied_for_tier
|
||||
user_id = auth.uid()
|
||||
);
|
||||
|
||||
-- Public can see tier info (for display in ads)
|
||||
CREATE POLICY "p2p_merchant_public_tier" ON public.p2p_merchant_tiers
|
||||
FOR SELECT USING (true);
|
||||
|
||||
-- Admins can manage all
|
||||
CREATE POLICY "p2p_merchant_admin" ON public.p2p_merchant_tiers
|
||||
FOR ALL USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.admin_roles
|
||||
WHERE user_id = auth.uid() AND role IN ('admin')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- MERCHANT STATS TABLE (Rolling 30-day stats)
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_merchant_stats (
|
||||
user_id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Volume stats (30 day rolling)
|
||||
total_volume_30d DECIMAL(18,2) DEFAULT 0,
|
||||
total_trades_30d INT DEFAULT 0,
|
||||
buy_volume_30d DECIMAL(18,2) DEFAULT 0,
|
||||
sell_volume_30d DECIMAL(18,2) DEFAULT 0,
|
||||
|
||||
-- Performance metrics
|
||||
completion_rate_30d DECIMAL(5,2) DEFAULT 0,
|
||||
avg_release_time_minutes INT,
|
||||
avg_payment_time_minutes INT,
|
||||
|
||||
-- Lifetime stats
|
||||
total_volume_lifetime DECIMAL(18,2) DEFAULT 0,
|
||||
total_trades_lifetime INT DEFAULT 0,
|
||||
|
||||
-- Ranking
|
||||
volume_rank INT,
|
||||
trade_count_rank INT,
|
||||
|
||||
-- Timestamps
|
||||
last_calculated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_merchant_stats_volume ON public.p2p_merchant_stats(total_volume_30d DESC);
|
||||
CREATE INDEX idx_merchant_stats_trades ON public.p2p_merchant_stats(total_trades_30d DESC);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_merchant_stats ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Public read (for leaderboards)
|
||||
CREATE POLICY "p2p_merchant_stats_public" ON public.p2p_merchant_stats
|
||||
FOR SELECT USING (true);
|
||||
|
||||
-- =====================================================
|
||||
-- FEATURED ADS TABLE
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_featured_ads (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
offer_id UUID NOT NULL REFERENCES public.p2p_fiat_offers(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id),
|
||||
|
||||
-- Featuring details
|
||||
position INT DEFAULT 1, -- 1 = top, 2 = second, etc.
|
||||
start_at TIMESTAMPTZ NOT NULL,
|
||||
end_at TIMESTAMPTZ NOT NULL,
|
||||
|
||||
-- Payment
|
||||
fee_amount DECIMAL(18,2) NOT NULL,
|
||||
fee_token VARCHAR(10) DEFAULT 'HEZ',
|
||||
fee_tx_hash TEXT,
|
||||
paid_at TIMESTAMPTZ,
|
||||
|
||||
-- Status
|
||||
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'expired', 'cancelled')),
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_featured_ads_active ON public.p2p_featured_ads(status, start_at, end_at)
|
||||
WHERE status = 'active';
|
||||
CREATE INDEX idx_featured_ads_offer ON public.p2p_featured_ads(offer_id);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_featured_ads ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Users can view active featured ads
|
||||
CREATE POLICY "p2p_featured_public_read" ON public.p2p_featured_ads
|
||||
FOR SELECT USING (status = 'active' OR user_id = auth.uid());
|
||||
|
||||
-- Users can create featured ads for own offers
|
||||
CREATE POLICY "p2p_featured_own_create" ON public.p2p_featured_ads
|
||||
FOR INSERT WITH CHECK (
|
||||
user_id = auth.uid() AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.p2p_fiat_offers
|
||||
WHERE id = offer_id AND seller_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- USER PAYMENT METHODS (Saved methods)
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_user_payment_methods (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
payment_method_id UUID NOT NULL REFERENCES public.payment_methods(id),
|
||||
|
||||
-- Account details (encrypted)
|
||||
account_details_encrypted TEXT NOT NULL,
|
||||
account_name TEXT, -- For display/verification
|
||||
|
||||
-- Settings
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
is_verified BOOLEAN DEFAULT FALSE,
|
||||
verified_at TIMESTAMPTZ,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_user_payment_methods_user ON public.p2p_user_payment_methods(user_id);
|
||||
CREATE UNIQUE INDEX idx_user_payment_methods_default ON public.p2p_user_payment_methods(user_id)
|
||||
WHERE is_default = true;
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE public.p2p_user_payment_methods ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Users can manage own payment methods
|
||||
CREATE POLICY "p2p_user_payment_own" ON public.p2p_user_payment_methods
|
||||
FOR ALL USING (user_id = auth.uid());
|
||||
|
||||
-- =====================================================
|
||||
-- TIER REQUIREMENTS CONSTANTS
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS public.p2p_tier_requirements (
|
||||
tier VARCHAR(20) PRIMARY KEY,
|
||||
min_trades INT NOT NULL,
|
||||
min_completion_rate DECIMAL(5,2) NOT NULL,
|
||||
min_volume_30d DECIMAL(18,2) NOT NULL,
|
||||
deposit_required DECIMAL(18,2) NOT NULL,
|
||||
deposit_token VARCHAR(10) DEFAULT 'HEZ',
|
||||
max_pending_orders INT NOT NULL,
|
||||
max_order_amount DECIMAL(18,2) NOT NULL,
|
||||
featured_ads_allowed INT NOT NULL,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
-- Insert tier requirements
|
||||
INSERT INTO public.p2p_tier_requirements (tier, min_trades, min_completion_rate, min_volume_30d, deposit_required, max_pending_orders, max_order_amount, featured_ads_allowed, description)
|
||||
VALUES
|
||||
('lite', 0, 0, 0, 0, 5, 10000, 0, 'Basic tier for all verified users'),
|
||||
('super', 20, 90, 5000, 10000, 20, 100000, 3, 'Professional trader tier with higher limits'),
|
||||
('diamond', 100, 95, 25000, 50000, 50, 150000, 10, 'Elite merchant tier with maximum privileges')
|
||||
ON CONFLICT (tier) DO UPDATE SET
|
||||
min_trades = EXCLUDED.min_trades,
|
||||
min_completion_rate = EXCLUDED.min_completion_rate,
|
||||
min_volume_30d = EXCLUDED.min_volume_30d,
|
||||
deposit_required = EXCLUDED.deposit_required,
|
||||
max_pending_orders = EXCLUDED.max_pending_orders,
|
||||
max_order_amount = EXCLUDED.max_order_amount,
|
||||
featured_ads_allowed = EXCLUDED.featured_ads_allowed,
|
||||
description = EXCLUDED.description;
|
||||
|
||||
-- =====================================================
|
||||
-- FUNCTION: Check Tier Eligibility
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.check_tier_eligibility(
|
||||
p_user_id UUID,
|
||||
p_target_tier VARCHAR(20)
|
||||
) RETURNS TABLE(
|
||||
eligible BOOLEAN,
|
||||
missing_requirements TEXT[]
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_reputation RECORD;
|
||||
v_stats RECORD;
|
||||
v_requirements RECORD;
|
||||
v_missing TEXT[] := '{}';
|
||||
BEGIN
|
||||
-- Get requirements
|
||||
SELECT * INTO v_requirements
|
||||
FROM public.p2p_tier_requirements
|
||||
WHERE tier = p_target_tier;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
eligible := FALSE;
|
||||
missing_requirements := ARRAY['Invalid tier'];
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Get user reputation
|
||||
SELECT * INTO v_reputation
|
||||
FROM public.p2p_reputation
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
-- Get user stats
|
||||
SELECT * INTO v_stats
|
||||
FROM public.p2p_merchant_stats
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
-- Check completed trades
|
||||
IF COALESCE(v_reputation.completed_trades, 0) < v_requirements.min_trades THEN
|
||||
v_missing := array_append(v_missing,
|
||||
format('Need %s completed trades (have %s)',
|
||||
v_requirements.min_trades,
|
||||
COALESCE(v_reputation.completed_trades, 0)));
|
||||
END IF;
|
||||
|
||||
-- Check completion rate
|
||||
IF COALESCE(v_stats.completion_rate_30d, 0) < v_requirements.min_completion_rate THEN
|
||||
v_missing := array_append(v_missing,
|
||||
format('Need %s%% completion rate (have %s%%)',
|
||||
v_requirements.min_completion_rate,
|
||||
COALESCE(v_stats.completion_rate_30d, 0)));
|
||||
END IF;
|
||||
|
||||
-- Check 30-day volume
|
||||
IF COALESCE(v_stats.total_volume_30d, 0) < v_requirements.min_volume_30d THEN
|
||||
v_missing := array_append(v_missing,
|
||||
format('Need $%s 30-day volume (have $%s)',
|
||||
v_requirements.min_volume_30d,
|
||||
COALESCE(v_stats.total_volume_30d, 0)));
|
||||
END IF;
|
||||
|
||||
-- Check deposit requirement
|
||||
IF v_requirements.deposit_required > 0 THEN
|
||||
v_missing := array_append(v_missing,
|
||||
format('Deposit of %s %s required',
|
||||
v_requirements.deposit_required,
|
||||
v_requirements.deposit_token));
|
||||
END IF;
|
||||
|
||||
eligible := array_length(v_missing, 1) IS NULL OR array_length(v_missing, 1) = 0;
|
||||
missing_requirements := v_missing;
|
||||
|
||||
RETURN NEXT;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- FUNCTION: Apply for Tier Upgrade
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.apply_for_tier_upgrade(
|
||||
p_user_id UUID,
|
||||
p_target_tier VARCHAR(20)
|
||||
) RETURNS TABLE(
|
||||
success BOOLEAN,
|
||||
message TEXT
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_eligibility RECORD;
|
||||
v_current_tier RECORD;
|
||||
BEGIN
|
||||
-- Check current tier
|
||||
SELECT * INTO v_current_tier
|
||||
FROM public.p2p_merchant_tiers
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
-- Check if already at or above target tier
|
||||
IF v_current_tier IS NOT NULL THEN
|
||||
IF v_current_tier.tier = p_target_tier THEN
|
||||
success := FALSE;
|
||||
message := 'You are already at this tier';
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF v_current_tier.application_status = 'pending' THEN
|
||||
success := FALSE;
|
||||
message := 'You already have a pending application';
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Check eligibility
|
||||
SELECT * INTO v_eligibility
|
||||
FROM public.check_tier_eligibility(p_user_id, p_target_tier);
|
||||
|
||||
IF NOT v_eligibility.eligible THEN
|
||||
success := FALSE;
|
||||
message := 'Not eligible: ' || array_to_string(v_eligibility.missing_requirements, ', ');
|
||||
RETURN NEXT;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Create or update application
|
||||
INSERT INTO public.p2p_merchant_tiers (
|
||||
user_id, application_status, applied_at, applied_for_tier
|
||||
) VALUES (
|
||||
p_user_id, 'pending', NOW(), p_target_tier
|
||||
)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
application_status = 'pending',
|
||||
applied_at = NOW(),
|
||||
applied_for_tier = p_target_tier,
|
||||
updated_at = NOW();
|
||||
|
||||
success := TRUE;
|
||||
message := 'Application submitted successfully';
|
||||
|
||||
RETURN NEXT;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- FUNCTION: Approve Tier Application (Admin)
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.approve_tier_application(
|
||||
p_user_id UUID,
|
||||
p_admin_id UUID
|
||||
) RETURNS void AS $$
|
||||
DECLARE
|
||||
v_application RECORD;
|
||||
v_requirements RECORD;
|
||||
BEGIN
|
||||
-- Get application
|
||||
SELECT * INTO v_application
|
||||
FROM public.p2p_merchant_tiers
|
||||
WHERE user_id = p_user_id AND application_status = 'pending';
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'No pending application found';
|
||||
END IF;
|
||||
|
||||
-- Get tier requirements
|
||||
SELECT * INTO v_requirements
|
||||
FROM public.p2p_tier_requirements
|
||||
WHERE tier = v_application.applied_for_tier;
|
||||
|
||||
-- Update tier
|
||||
UPDATE public.p2p_merchant_tiers
|
||||
SET
|
||||
tier = v_application.applied_for_tier,
|
||||
application_status = 'approved',
|
||||
approved_at = NOW(),
|
||||
approved_by = p_admin_id,
|
||||
max_pending_orders = v_requirements.max_pending_orders,
|
||||
max_order_amount = v_requirements.max_order_amount,
|
||||
featured_ads_allowed = v_requirements.featured_ads_allowed,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = p_user_id;
|
||||
|
||||
-- Create notification
|
||||
PERFORM public.create_p2p_notification(
|
||||
p_user_id,
|
||||
'system',
|
||||
'Tier Upgrade Approved!',
|
||||
format('Congratulations! You have been upgraded to %s tier.', v_application.applied_for_tier),
|
||||
NULL,
|
||||
NULL,
|
||||
'/p2p/merchant'
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- FUNCTION: Calculate Merchant Stats
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION public.calculate_merchant_stats(p_user_id UUID)
|
||||
RETURNS void AS $$
|
||||
DECLARE
|
||||
v_stats RECORD;
|
||||
BEGIN
|
||||
-- Calculate 30-day stats
|
||||
SELECT
|
||||
COUNT(*) as trades_30d,
|
||||
COALESCE(SUM(fiat_amount), 0) as volume_30d,
|
||||
COALESCE(SUM(CASE WHEN buyer_id = p_user_id THEN fiat_amount ELSE 0 END), 0) as buy_volume,
|
||||
COALESCE(SUM(CASE WHEN seller_id = p_user_id THEN fiat_amount ELSE 0 END), 0) as sell_volume,
|
||||
COALESCE(
|
||||
(COUNT(*) FILTER (WHERE status = 'completed')::DECIMAL /
|
||||
NULLIF(COUNT(*), 0) * 100), 0
|
||||
) as completion_rate,
|
||||
AVG(EXTRACT(EPOCH FROM (seller_confirmed_at - buyer_marked_paid_at)) / 60)
|
||||
FILTER (WHERE seller_id = p_user_id AND seller_confirmed_at IS NOT NULL) as avg_release,
|
||||
AVG(EXTRACT(EPOCH FROM (buyer_marked_paid_at - created_at)) / 60)
|
||||
FILTER (WHERE buyer_id = p_user_id AND buyer_marked_paid_at IS NOT NULL) as avg_payment
|
||||
INTO v_stats
|
||||
FROM public.p2p_fiat_trades
|
||||
WHERE (buyer_id = p_user_id OR seller_id = p_user_id)
|
||||
AND created_at >= NOW() - INTERVAL '30 days';
|
||||
|
||||
-- Upsert stats
|
||||
INSERT INTO public.p2p_merchant_stats (
|
||||
user_id,
|
||||
total_volume_30d,
|
||||
total_trades_30d,
|
||||
buy_volume_30d,
|
||||
sell_volume_30d,
|
||||
completion_rate_30d,
|
||||
avg_release_time_minutes,
|
||||
avg_payment_time_minutes,
|
||||
last_calculated_at
|
||||
) VALUES (
|
||||
p_user_id,
|
||||
v_stats.volume_30d,
|
||||
v_stats.trades_30d,
|
||||
v_stats.buy_volume,
|
||||
v_stats.sell_volume,
|
||||
v_stats.completion_rate,
|
||||
v_stats.avg_release::INT,
|
||||
v_stats.avg_payment::INT,
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
total_volume_30d = v_stats.volume_30d,
|
||||
total_trades_30d = v_stats.trades_30d,
|
||||
buy_volume_30d = v_stats.buy_volume,
|
||||
sell_volume_30d = v_stats.sell_volume,
|
||||
completion_rate_30d = v_stats.completion_rate,
|
||||
avg_release_time_minutes = v_stats.avg_release::INT,
|
||||
avg_payment_time_minutes = v_stats.avg_payment::INT,
|
||||
last_calculated_at = NOW();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- =====================================================
|
||||
-- TRIGGER: Update stats on trade completion
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION update_merchant_stats_on_trade()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.status = 'completed' AND (OLD.status IS NULL OR OLD.status != 'completed') THEN
|
||||
PERFORM public.calculate_merchant_stats(NEW.buyer_id);
|
||||
PERFORM public.calculate_merchant_stats(NEW.seller_id);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
CREATE TRIGGER trigger_update_merchant_stats
|
||||
AFTER UPDATE ON public.p2p_fiat_trades
|
||||
FOR EACH ROW EXECUTE FUNCTION update_merchant_stats_on_trade();
|
||||
|
||||
-- =====================================================
|
||||
-- ADD is_featured COLUMN TO OFFERS
|
||||
-- =====================================================
|
||||
ALTER TABLE public.p2p_fiat_offers
|
||||
ADD COLUMN IF NOT EXISTS is_featured BOOLEAN DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE public.p2p_fiat_offers
|
||||
ADD COLUMN IF NOT EXISTS featured_until TIMESTAMPTZ;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_p2p_offers_featured ON public.p2p_fiat_offers(is_featured, featured_until)
|
||||
WHERE is_featured = true;
|
||||
|
||||
-- =====================================================
|
||||
-- GRANTS
|
||||
-- =====================================================
|
||||
GRANT EXECUTE ON FUNCTION public.check_tier_eligibility TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.apply_for_tier_upgrade TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.approve_tier_application TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.calculate_merchant_stats TO authenticated;
|
||||
|
||||
-- =====================================================
|
||||
-- COMMENTS
|
||||
-- =====================================================
|
||||
COMMENT ON TABLE public.p2p_merchant_tiers IS 'Merchant tier assignments and applications';
|
||||
COMMENT ON TABLE public.p2p_merchant_stats IS 'Rolling 30-day trading statistics for merchants';
|
||||
COMMENT ON TABLE public.p2p_featured_ads IS 'Featured/promoted P2P advertisements';
|
||||
COMMENT ON TABLE public.p2p_tier_requirements IS 'Requirements for each merchant tier level';
|
||||
Reference in New Issue
Block a user