643 lines
27 KiB
Python
643 lines
27 KiB
Python
# -*- coding: utf-8 -*-
|
|
import pytz
|
|
from odoo import http
|
|
from odoo.http import request
|
|
|
|
|
|
def normalize_phone_search(phone):
|
|
if not phone:
|
|
return []
|
|
digits = ''.join(c for c in phone if c.isdigit())
|
|
if not digits:
|
|
return []
|
|
candidates = [digits]
|
|
if digits.startswith('62'):
|
|
candidates.append('0' + digits[2:])
|
|
candidates.append(digits[2:])
|
|
elif digits.startswith('0'):
|
|
candidates.append('62' + digits[1:])
|
|
candidates.append(digits[1:])
|
|
else:
|
|
candidates.append('0' + digits)
|
|
candidates.append('62' + digits)
|
|
return list(set(candidates))
|
|
|
|
|
|
class AppNotificationController(http.Controller):
|
|
|
|
@http.route('/api/loyalty/fetch_notifications', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
|
def fetch_notifications(self, **kw):
|
|
"""
|
|
Endpoint for the Flutter app Background Task and In-App notification center.
|
|
Returns image_128 (base64 thumbnail) for list display.
|
|
The Flutter detail screen loads the full image via /web/image/ with session cookie.
|
|
Body is now HTML from the rich text editor — returned as-is.
|
|
"""
|
|
import base64 as b64mod
|
|
user = request.env.user
|
|
partner = user.partner_id
|
|
|
|
last_id = kw.get('last_id', 0)
|
|
|
|
# Give them global notifications OR notifications tagged explicitly to them
|
|
domain = [
|
|
('id', '>', last_id),
|
|
'|',
|
|
('partner_ids', 'in', [partner.id]),
|
|
('is_global', '=', True)
|
|
]
|
|
|
|
notifications = request.env['mapan.app.notification'].sudo().search_read(
|
|
domain,
|
|
['id', 'title', 'body', 'create_date', 'image_128'],
|
|
order='create_date desc',
|
|
limit=50
|
|
)
|
|
|
|
# Normalize image_128 bytes → base64 string for JSON transport
|
|
for notif in notifications:
|
|
img = notif.get('image_128')
|
|
if img and isinstance(img, bytes):
|
|
notif['image_128'] = img.decode('utf-8')
|
|
elif not img:
|
|
notif['image_128'] = None
|
|
notif['has_image'] = bool(notif['image_128'])
|
|
# body is HTML string — pass through as-is
|
|
if notif.get('body') is False:
|
|
notif['body'] = ''
|
|
|
|
return {
|
|
'status': 'success',
|
|
'data': notifications
|
|
}
|
|
|
|
@http.route('/api/loyalty/cms_content', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
|
def fetch_cms_content(self, **kw):
|
|
"""
|
|
Public endpoint to fetch carousel slides and promo highlights for the Flutter app home screen.
|
|
"""
|
|
import base64 as b64mod
|
|
from datetime import date
|
|
|
|
today = date.today()
|
|
|
|
# --- Carousel Slides ---
|
|
carousel_domain = [('is_active', '=', True)]
|
|
carousel_items = request.env['mapan.app.carousel'].sudo().search_read(
|
|
carousel_domain,
|
|
['id', 'name', 'image', 'image_url', 'body', 'sequence'],
|
|
order='sequence, id'
|
|
)
|
|
for item in carousel_items:
|
|
img = item.get('image')
|
|
if img and isinstance(img, bytes):
|
|
item['image'] = img.decode('utf-8')
|
|
elif not img:
|
|
item['image'] = None
|
|
if item.get('body') is False:
|
|
item['body'] = ''
|
|
|
|
# --- Promo Highlights ---
|
|
promo_domain = [('is_active', '=', True)]
|
|
promos = request.env['mapan.app.promo'].sudo().search_read(
|
|
promo_domain,
|
|
['id', 'name', 'body', 'image_128', 'date_start', 'date_end', 'sequence'],
|
|
order='sequence, id'
|
|
)
|
|
active_promos = []
|
|
for promo in promos:
|
|
# Filter by validity date client-side after fetch
|
|
ds = promo.get('date_start')
|
|
de = promo.get('date_end')
|
|
if ds and ds > today:
|
|
continue
|
|
if de and de < today:
|
|
continue
|
|
img = promo.get('image_128')
|
|
if img and isinstance(img, bytes):
|
|
promo['image_128'] = img.decode('utf-8')
|
|
elif not img:
|
|
promo['image_128'] = None
|
|
# Convert date objects to string
|
|
promo['date_start'] = str(promo['date_start']) if promo.get('date_start') else None
|
|
promo['date_end'] = str(promo['date_end']) if promo.get('date_end') else None
|
|
if promo.get('body') is False:
|
|
promo['body'] = ''
|
|
active_promos.append(promo)
|
|
|
|
return {
|
|
'status': 'success',
|
|
'carousel': carousel_items,
|
|
'promos': active_promos,
|
|
}
|
|
|
|
@http.route('/api/loyalty/app_config', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
|
def fetch_app_config(self, **kw):
|
|
"""
|
|
Public endpoint to fetch app configuration (About Us URL, Contact Us URL, Branding & Theme).
|
|
"""
|
|
config = request.env['mapan.app.config'].sudo().search([], limit=1)
|
|
if not config:
|
|
config = request.env['mapan.app.config'].sudo().create({
|
|
'name': 'App Configuration',
|
|
'primary_color': '#C62828',
|
|
'secondary_color': '#FF8F00'
|
|
})
|
|
|
|
brand_logo = config.brand_logo
|
|
if brand_logo and isinstance(brand_logo, bytes):
|
|
brand_logo = brand_logo.decode('utf-8')
|
|
|
|
return {
|
|
'status': 'success',
|
|
'about_us_url': config.about_us_url or '',
|
|
'contact_us_url': config.contact_us_url or '',
|
|
'brand_logo': brand_logo or '',
|
|
'primary_color': config.primary_color or '#C62828',
|
|
'secondary_color': config.secondary_color or '#FF8F00',
|
|
'background_color': config.background_color or '#FAF6EE',
|
|
'background_gradient_color': config.background_gradient_color or '#F3EAD3',
|
|
}
|
|
|
|
@http.route('/api/loyalty/order_history', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
|
def fetch_order_history(self, **kw):
|
|
"""
|
|
Authenticated endpoint to fetch loyalty point history for the current user.
|
|
Queries loyalty.history which tracks earn/spend events.
|
|
Positive points = earned, negative points = spent.
|
|
"""
|
|
user = request.env.user
|
|
partner = user.partner_id
|
|
|
|
# Get loyalty cards for this partner (only multi-level membership and subscription programs)
|
|
cards = request.env['loyalty.card'].sudo().search([
|
|
('partner_id', '=', partner.id),
|
|
'|',
|
|
('program_id.multi_level_membership', '=', True),
|
|
('program_id.program_type', '=', 'subscription')
|
|
])
|
|
card_data = {c.id: {'program_type': c.program_id.program_type, 'program_name': c.program_id.name} for c in cards}
|
|
card_ids = list(card_data.keys())
|
|
|
|
if not card_ids:
|
|
return {'status': 'success', 'data': []}
|
|
|
|
history_records = request.env['loyalty.history'].sudo().search_read(
|
|
[('card_id', 'in', card_ids)],
|
|
['id', 'card_id', 'issued', 'used', 'create_date', 'order_model', 'order_id', 'description'],
|
|
order='create_date desc',
|
|
limit=100
|
|
)
|
|
|
|
# Collect IDs to fetch in batches
|
|
pos_order_ids = []
|
|
sale_order_ids = []
|
|
for rec in history_records:
|
|
order_model = rec.get('order_model')
|
|
order_id = rec.get('order_id')
|
|
if order_id and order_model == 'pos.order':
|
|
pos_order_ids.append(order_id)
|
|
elif order_id and order_model == 'sale.order':
|
|
sale_order_ids.append(order_id)
|
|
|
|
# Batch query pos.orders to get receipt number and store name
|
|
pos_orders_map = {}
|
|
if pos_order_ids:
|
|
pos_orders = request.env['pos.order'].sudo().search_read(
|
|
[('id', 'in', pos_order_ids)],
|
|
['id', 'name', 'pos_reference', 'config_id']
|
|
)
|
|
for po in pos_orders:
|
|
config_name = po.get('config_id')[1] if isinstance(po.get('config_id'), (list, tuple)) else ''
|
|
pos_orders_map[po['id']] = {
|
|
'reference': po.get('pos_reference') or po.get('name') or '',
|
|
'pos_name': config_name
|
|
}
|
|
|
|
# Batch query sale.orders to get order number and warehouse name
|
|
sale_orders_map = {}
|
|
if sale_order_ids:
|
|
sale_orders = request.env['sale.order'].sudo().search_read(
|
|
[('id', 'in', sale_order_ids)],
|
|
['id', 'name', 'warehouse_id']
|
|
)
|
|
for so in sale_orders:
|
|
wh_name = so.get('warehouse_id')[1] if isinstance(so.get('warehouse_id'), (list, tuple)) else ''
|
|
sale_orders_map[so['id']] = {
|
|
'reference': so.get('name') or '',
|
|
'pos_name': wh_name
|
|
}
|
|
|
|
result = []
|
|
for rec in history_records:
|
|
issued = rec.get('issued') or 0.0
|
|
used = rec.get('used') or 0.0
|
|
points = issued - used
|
|
point_type = 'earn' if points >= 0 else 'spend'
|
|
|
|
order_model = rec.get('order_model')
|
|
order_id = rec.get('order_id')
|
|
|
|
order_ref = ''
|
|
pos_name = ''
|
|
|
|
if order_id and order_model == 'pos.order' and order_id in pos_orders_map:
|
|
order_ref = pos_orders_map[order_id]['reference']
|
|
pos_name = pos_orders_map[order_id]['pos_name']
|
|
elif order_id and order_model == 'sale.order' and order_id in sale_orders_map:
|
|
order_ref = sale_orders_map[order_id]['reference']
|
|
pos_name = sale_orders_map[order_id]['pos_name']
|
|
|
|
# Fallbacks
|
|
if not order_ref:
|
|
if order_id and isinstance(order_id, (list, tuple)) and len(order_id) > 1:
|
|
order_ref = str(order_id[1])
|
|
elif order_id:
|
|
order_ref = str(order_id)
|
|
else:
|
|
order_ref = rec.get('description') or ''
|
|
|
|
card_id_val = rec['card_id'][0] if isinstance(rec.get('card_id'), (list, tuple)) else rec.get('card_id')
|
|
card_info = card_data.get(card_id_val, {})
|
|
program_type = card_info.get('program_type') or 'loyalty'
|
|
program_name = card_info.get('program_name') or ''
|
|
|
|
create_date = rec.get('create_date')
|
|
if create_date:
|
|
if create_date.tzinfo is None:
|
|
create_date = pytz.utc.localize(create_date)
|
|
wib_tz = pytz.timezone('Asia/Jakarta')
|
|
create_date = create_date.astimezone(wib_tz)
|
|
date_str = create_date.strftime('%Y-%m-%d %H:%M:%S')
|
|
else:
|
|
date_str = ''
|
|
|
|
result.append({
|
|
'id': rec['id'],
|
|
'date': date_str,
|
|
'points': round(float(points), 2),
|
|
'type': point_type,
|
|
'order_ref': order_ref,
|
|
'pos_name': pos_name,
|
|
'card_id': card_id_val,
|
|
'program_type': program_type,
|
|
'program_name': program_name,
|
|
})
|
|
|
|
return {'status': 'success', 'data': result}
|
|
|
|
@http.route('/api/loyalty/branches', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
|
def fetch_branches(self, **kw):
|
|
"""
|
|
Public endpoint for the Flutter app to get branches without exposing API keys.
|
|
Includes latitude/longitude for geolocation-based distance sorting on the client.
|
|
"""
|
|
try:
|
|
companies = request.env['res.company'].sudo().search([
|
|
('parent_id', '!=', False)
|
|
])
|
|
data = []
|
|
for comp in companies:
|
|
pos = request.env['pos.config'].sudo().search([
|
|
('company_id', '=', comp.id)
|
|
], limit=1)
|
|
data.append({
|
|
'id': comp.id,
|
|
'name': comp.name,
|
|
'street': comp.street or '',
|
|
'city': comp.city or '',
|
|
'phone': comp.phone or '',
|
|
'partner_latitude': pos.branch_latitude if pos else 0.0,
|
|
'partner_longitude': pos.branch_longitude if pos else 0.0,
|
|
})
|
|
return {'status': 'success', 'data': data}
|
|
except Exception as e:
|
|
return {'status': 'error', 'message': str(e)}
|
|
|
|
@http.route('/api/loyalty/send_otp', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
|
def send_otp(self, **kw):
|
|
"""
|
|
Public endpoint to send a verification OTP to an email address.
|
|
"""
|
|
email = kw.get('email')
|
|
phone = kw.get('phone')
|
|
phone_or_email = kw.get('phone_or_email')
|
|
otp_type = kw.get('type')
|
|
|
|
if not otp_type:
|
|
return {'status': 'error', 'message': 'OTP type is required.'}
|
|
|
|
if otp_type == 'signup':
|
|
if not email or not phone:
|
|
return {'status': 'error', 'message': 'Email and Phone are required for sign-up.'}
|
|
# Check if partner or user already exists
|
|
candidates = normalize_phone_search(phone)
|
|
existing_partner = request.env['res.partner'].sudo().search([
|
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
|
], limit=1)
|
|
if existing_partner:
|
|
return {'status': 'error', 'message': 'A member with this phone number is already registered.'}
|
|
|
|
existing_user = request.env['res.users'].sudo().with_context(active_test=False).search([
|
|
'|', ('login', '=', phone), ('login', 'in', candidates), ('email', '=', email)
|
|
], limit=1)
|
|
if existing_user:
|
|
return {'status': 'error', 'message': 'A user with this phone number or email already exists.'}
|
|
|
|
elif otp_type == 'activation':
|
|
if not email or not phone:
|
|
return {'status': 'error', 'message': 'Email and Phone are required for activation.'}
|
|
# Check if partner exists
|
|
candidates = normalize_phone_search(phone)
|
|
partner = request.env['res.partner'].sudo().search([
|
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
|
], limit=1)
|
|
if not partner:
|
|
return {'status': 'error', 'message': 'Member with this phone number not found. Please contact support or register.'}
|
|
|
|
# Check if user already active
|
|
user = request.env['res.users'].sudo().with_context(active_test=False).search([('partner_id', '=', partner.id)], limit=1)
|
|
if user and user.active:
|
|
return {'status': 'error', 'message': 'Account is already active. Please login or reset password.'}
|
|
|
|
# Check if email is already taken by another active user
|
|
existing_email_user = request.env['res.users'].sudo().search([('email', '=', email), ('partner_id', '!=', partner.id)], limit=1)
|
|
if existing_email_user:
|
|
return {'status': 'error', 'message': 'This email address is already registered to another account.'}
|
|
|
|
elif otp_type == 'reset_password':
|
|
if not phone_or_email:
|
|
return {'status': 'error', 'message': 'Phone number or Email is required.'}
|
|
|
|
# Find user
|
|
user = None
|
|
if '@' in phone_or_email:
|
|
user = request.env['res.users'].sudo().search([('email', '=', phone_or_email)], limit=1)
|
|
else:
|
|
candidates = normalize_phone_search(phone_or_email)
|
|
user = request.env['res.users'].sudo().search([('login', 'in', candidates)], limit=1)
|
|
if not user:
|
|
partner = request.env['res.partner'].sudo().search([
|
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
|
], limit=1)
|
|
if partner:
|
|
user = request.env['res.users'].sudo().search([('partner_id', '=', partner.id)], limit=1)
|
|
|
|
if not user:
|
|
return {'status': 'error', 'message': 'User account not found.'}
|
|
|
|
email = user.email
|
|
if not email or '@miemapan.com' in email or '@' not in email:
|
|
return {'status': 'error', 'message': 'Your account does not have a valid email set. Please contact support.'}
|
|
|
|
phone = user.login
|
|
|
|
try:
|
|
request.env['loyalty.verification.otp'].sudo().generate_otp(email=email, phone=phone, otp_type=otp_type)
|
|
# Mask email for privacy response
|
|
parts = email.split('@')
|
|
masked_email = parts[0][:2] + '*' * (len(parts[0]) - 2) + '@' + parts[1] if len(parts[0]) > 2 else email
|
|
return {
|
|
'status': 'success',
|
|
'message': f'Verification code sent to {masked_email}.',
|
|
'email': email
|
|
}
|
|
except Exception as e:
|
|
return {'status': 'error', 'message': f'Failed to send verification code: {str(e)}'}
|
|
|
|
@http.route('/api/loyalty/activate_account', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
|
def activate_account(self, **kw):
|
|
"""
|
|
Public endpoint to activate/set password for an existing partner.
|
|
"""
|
|
phone = kw.get('phone')
|
|
email = kw.get('email')
|
|
password = kw.get('password')
|
|
birth_date = kw.get('birth_date')
|
|
otp = kw.get('otp')
|
|
|
|
if not phone or not password or not email or not otp:
|
|
return {'status': 'error', 'message': 'Phone, Email, Password, and Verification Code (OTP) are required.'}
|
|
|
|
# Verify OTP first
|
|
otp_verified = request.env['loyalty.verification.otp'].sudo().verify_otp(
|
|
otp_code=otp, email=email, phone=phone, otp_type='activation'
|
|
)
|
|
if not otp_verified:
|
|
return {'status': 'error', 'message': 'Invalid or expired verification code.'}
|
|
|
|
candidates = normalize_phone_search(phone)
|
|
partner = request.env['res.partner'].sudo().search([
|
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
|
], limit=1)
|
|
|
|
if not partner:
|
|
return {'status': 'error', 'message': 'Member with this phone number not found. Please contact support or register.'}
|
|
|
|
# Verify birth date if set on the partner
|
|
if partner.birth_date:
|
|
if not birth_date:
|
|
return {'status': 'error', 'message': 'Birth Date is required for verification.'}
|
|
if str(partner.birth_date) != str(birth_date):
|
|
return {'status': 'error', 'message': 'Birth Date does not match our records.'}
|
|
|
|
# Update email on partner
|
|
partner.sudo().write({'email': email})
|
|
|
|
# Find or create user
|
|
user = request.env['res.users'].sudo().with_context(active_test=False).search([('partner_id', '=', partner.id)], limit=1)
|
|
if user:
|
|
user.sudo().write({
|
|
'company_id': request.env.company.id,
|
|
'company_ids': [(6, 0, [request.env.company.id])],
|
|
'login': phone,
|
|
'email': email,
|
|
'password': password,
|
|
'active': True
|
|
})
|
|
portal_group = request.env.ref('base.group_portal')
|
|
if portal_group not in user.group_ids:
|
|
user.sudo().write({'group_ids': [(4, portal_group.id)]})
|
|
else:
|
|
portal_template = request.env.ref('base.template_portal_user_id')
|
|
existing_user = request.env['res.users'].sudo().with_context(active_test=False).search([('login', '=', phone)], limit=1)
|
|
if existing_user:
|
|
return {'status': 'error', 'message': 'A user with this phone number login already exists.'}
|
|
|
|
try:
|
|
user = portal_template.sudo().with_context(no_reset_password=True).copy({
|
|
'name': partner.name,
|
|
'login': phone,
|
|
'email': email,
|
|
'partner_id': partner.id,
|
|
'active': False,
|
|
})
|
|
# Write company, password and activate
|
|
user.sudo().write({
|
|
'company_id': request.env.company.id,
|
|
'company_ids': [(6, 0, [request.env.company.id])],
|
|
'password': password,
|
|
'active': True,
|
|
})
|
|
# Ensure portal group
|
|
portal_group = request.env.ref('base.group_portal')
|
|
if portal_group not in user.group_ids:
|
|
user.sudo().write({'group_ids': [(4, portal_group.id)]})
|
|
except Exception as e:
|
|
return {'status': 'error', 'message': f'Failed to create user: {str(e)}'}
|
|
|
|
return {'status': 'success', 'message': 'Account activated successfully. You can now login.'}
|
|
|
|
@http.route('/api/loyalty/signup_member', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
|
def signup_member(self, **kw):
|
|
"""
|
|
Public endpoint to sign up a new member.
|
|
"""
|
|
name = kw.get('name')
|
|
phone = kw.get('phone')
|
|
email = kw.get('email')
|
|
birth_date = kw.get('birth_date')
|
|
gender = kw.get('gender')
|
|
password = kw.get('password')
|
|
otp = kw.get('otp')
|
|
|
|
if not name or not phone or not email or not password or not otp:
|
|
return {'status': 'error', 'message': 'Name, Phone, Email, Password, and Verification Code (OTP) are required.'}
|
|
|
|
# Verify OTP first
|
|
otp_verified = request.env['loyalty.verification.otp'].sudo().verify_otp(
|
|
otp_code=otp, email=email, phone=phone, otp_type='signup'
|
|
)
|
|
if not otp_verified:
|
|
return {'status': 'error', 'message': 'Invalid or expired verification code.'}
|
|
|
|
candidates = normalize_phone_search(phone)
|
|
|
|
existing_partner = request.env['res.partner'].sudo().search([
|
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
|
], limit=1)
|
|
if existing_partner:
|
|
return {'status': 'error', 'message': 'A member with this phone number is already registered.'}
|
|
|
|
existing_user = request.env['res.users'].sudo().with_context(active_test=False).search([
|
|
'|', ('login', '=', phone), ('login', 'in', candidates), ('email', '=', email)
|
|
], limit=1)
|
|
if existing_user:
|
|
return {'status': 'error', 'message': 'A user with this phone number or email already exists.'}
|
|
|
|
try:
|
|
partner_vals = {
|
|
'name': name,
|
|
'phone': phone,
|
|
'email': email,
|
|
'birth_date': birth_date,
|
|
'gender': gender,
|
|
'company_id': request.env.company.id,
|
|
}
|
|
partner = request.env['res.partner'].sudo().create(partner_vals)
|
|
|
|
portal_template = request.env.ref('base.template_portal_user_id')
|
|
user = portal_template.sudo().with_context(no_reset_password=True).copy({
|
|
'name': partner.name,
|
|
'login': phone,
|
|
'email': email,
|
|
'partner_id': partner.id,
|
|
'active': False,
|
|
})
|
|
# Write company, password and activate
|
|
user.sudo().write({
|
|
'company_id': request.env.company.id,
|
|
'company_ids': [(6, 0, [request.env.company.id])],
|
|
'password': password,
|
|
'active': True,
|
|
})
|
|
# Ensure portal group
|
|
portal_group = request.env.ref('base.group_portal')
|
|
if portal_group not in user.group_ids:
|
|
user.sudo().write({'group_ids': [(4, portal_group.id)]})
|
|
|
|
return {'status': 'success', 'message': 'Account registered successfully.'}
|
|
except Exception as e:
|
|
return {'status': 'error', 'message': f'Registration failed: {str(e)}'}
|
|
|
|
@http.route('/api/loyalty/reset_password', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
|
def reset_password(self, **kw):
|
|
"""
|
|
Public endpoint to reset user password using verification OTP.
|
|
"""
|
|
phone_or_email = kw.get('phone_or_email')
|
|
otp = kw.get('otp')
|
|
new_password = kw.get('password')
|
|
|
|
if not phone_or_email or not otp or not new_password:
|
|
return {'status': 'error', 'message': 'Phone/Email, OTP, and New Password are required.'}
|
|
|
|
# Find user
|
|
user = None
|
|
if '@' in phone_or_email:
|
|
user = request.env['res.users'].sudo().search([('email', '=', phone_or_email)], limit=1)
|
|
else:
|
|
candidates = normalize_phone_search(phone_or_email)
|
|
user = request.env['res.users'].sudo().search([('login', 'in', candidates)], limit=1)
|
|
if not user:
|
|
partner = request.env['res.partner'].sudo().search([
|
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
|
], limit=1)
|
|
if partner:
|
|
user = request.env['res.users'].sudo().search([('partner_id', '=', partner.id)], limit=1)
|
|
|
|
if not user:
|
|
return {'status': 'error', 'message': 'User account not found.'}
|
|
|
|
# Verify OTP
|
|
otp_verified = request.env['loyalty.verification.otp'].sudo().verify_otp(
|
|
otp_code=otp, email=user.email, phone=user.login, otp_type='reset_password'
|
|
)
|
|
if not otp_verified:
|
|
return {'status': 'error', 'message': 'Invalid or expired verification code.'}
|
|
|
|
try:
|
|
user.sudo().write({
|
|
'password': new_password,
|
|
'active': True
|
|
})
|
|
return {'status': 'success', 'message': 'Password has been reset successfully.'}
|
|
except Exception as e:
|
|
return {'status': 'error', 'message': f'Failed to reset password: {str(e)}'}
|
|
|
|
@http.route('/api/loyalty/delete_account', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
|
def delete_account(self, **kw):
|
|
"""
|
|
Authenticated endpoint to permanently delete/anonymize a member account.
|
|
"""
|
|
user = request.env.user
|
|
password = kw.get('password')
|
|
|
|
if not password:
|
|
return {'status': 'error', 'message': 'Password is required.'}
|
|
|
|
try:
|
|
user.with_user(user)._check_credentials({'type': 'password', 'password': password}, {'interactive': True})
|
|
except Exception:
|
|
return {'status': 'error', 'message': 'Incorrect password.'}
|
|
|
|
partner = user.partner_id
|
|
|
|
user.sudo().write({'active': False})
|
|
|
|
partner.sudo().write({
|
|
'name': f'Deleted Member {partner.id}',
|
|
'phone': False,
|
|
'email': False,
|
|
'birth_date': False,
|
|
'gender': False,
|
|
'active': False,
|
|
})
|
|
|
|
partner.loyalty_card_ids.sudo().write({
|
|
'active': False,
|
|
'points': 0.0
|
|
})
|
|
|
|
return {'status': 'success', 'message': 'Account deleted successfully.'}
|