Рефакторинг системы кошелька клиентов
Основные изменения: - Переход от денормализованного поля wallet_balance к вычисляемому балансу - Баланс теперь вычисляется как SUM(signed_amount) транзакций - Добавлено кеширование баланса для производительности (5 минут) - Новая модель WalletTransaction с полем signed_amount (может быть +/-) - WalletService для всех операций с кошельком (deposit, spend, adjustment) - Защита от отрицательного баланса и race conditions через select_for_update - Добавлен balance_after в каждую транзакцию для аудита - Обновлены миграции для переноса данных из старой схемы Улучшения безопасности: - Атомарные транзакции для всех операций с балансом - Блокировка строк при модификации баланса - Валидация недостаточности средств - Обязательное описание для корректировок баланса UI/UX изменения: - Обновлён вывод баланса кошелька в деталях клиента - Добавлена история транзакций с типами и описаниями - Цветовая индикация положительных транзакций (зелёный) Техническая документация: - Добавлены docstrings для всех методов WalletService - Комментарии к критичным участкам кода - Примеры использования в docstrings
This commit is contained in:
@@ -74,6 +74,60 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Каналы связи -->
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Каналы связи</h5>
|
||||
<button class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#addChannelModal">
|
||||
<i class="bi bi-plus"></i> Добавить
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contact_channels %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for channel in contact_channels %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{% if channel.channel_type == 'telegram' %}
|
||||
<span class="badge bg-info me-2"><i class="bi bi-telegram"></i> Telegram</span>
|
||||
{% elif channel.channel_type == 'instagram' %}
|
||||
<span class="badge bg-danger me-2"><i class="bi bi-instagram"></i> Instagram</span>
|
||||
{% elif channel.channel_type == 'whatsapp' %}
|
||||
<span class="badge bg-success me-2"><i class="bi bi-whatsapp"></i> WhatsApp</span>
|
||||
{% elif channel.channel_type == 'viber' %}
|
||||
<span class="badge bg-purple me-2" style="background-color: #7360f2 !important;"><i class="bi bi-chat-fill"></i> Viber</span>
|
||||
{% elif channel.channel_type == 'vk' %}
|
||||
<span class="badge bg-primary me-2">VK</span>
|
||||
{% elif channel.channel_type == 'facebook' %}
|
||||
<span class="badge bg-primary me-2"><i class="bi bi-facebook"></i> Facebook</span>
|
||||
{% elif channel.channel_type == 'phone' %}
|
||||
<span class="badge bg-secondary me-2"><i class="bi bi-telephone"></i> Телефон</span>
|
||||
{% elif channel.channel_type == 'email' %}
|
||||
<span class="badge bg-secondary me-2"><i class="bi bi-envelope"></i> Email</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark me-2">{{ channel.get_channel_type_display }}</span>
|
||||
{% endif %}
|
||||
<strong>{{ channel.value }}</strong>
|
||||
{% if channel.is_primary %}<span class="badge bg-warning text-dark ms-1">основной</span>{% endif %}
|
||||
{% if channel.notes %}<small class="text-muted d-block mt-1">{{ channel.notes }}</small>{% endif %}
|
||||
</div>
|
||||
<form method="post" action="{% url 'customers:delete-contact-channel' channel.pk %}" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Удалить канал?')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">Нет дополнительных каналов связи. Добавьте Instagram, Telegram и другие контакты.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Алерт о необходимости возврата -->
|
||||
{% if refund_amount > 0 %}
|
||||
<div class="col-md-12">
|
||||
@@ -436,6 +490,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно добавления канала связи -->
|
||||
<div class="modal fade" id="addChannelModal" tabindex="-1" aria-labelledby="addChannelModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="{% url 'customers:add-contact-channel' customer.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addChannelModalLabel">Добавить канал связи</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="channel_type" class="form-label">Тип канала</label>
|
||||
<select name="channel_type" id="channel_type" class="form-select" required>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="instagram">Instagram</option>
|
||||
<option value="whatsapp">WhatsApp</option>
|
||||
<option value="viber">Viber</option>
|
||||
<option value="vk">ВКонтакте</option>
|
||||
<option value="facebook">Facebook</option>
|
||||
<option value="phone">Телефон</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="channel_value" class="form-label">Значение</label>
|
||||
<input type="text" name="value" id="channel_value" class="form-control" placeholder="@username, номер, ссылка..." required>
|
||||
<small class="text-muted">Например: @flower_lover, +375291234567, flower.shop</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="channel_notes" class="form-label">Примечание <span class="text-muted">(необязательно)</span></label>
|
||||
<input type="text" name="notes" id="channel_notes" class="form-control" placeholder="Личный аккаунт, рабочий...">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="is_primary" class="form-check-input" id="isPrimary" value="true">
|
||||
<label class="form-check-label" for="isPrimary">Основной канал связи</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="submit" class="btn btn-success"><i class="bi bi-plus"></i> Добавить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Автооткрытие collapse при наличии якоря в URL
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
Reference in New Issue
Block a user